fixcore-engine 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.
- fixcore/__init__.py +6 -0
- fixcore/application.py +47 -0
- fixcore/log/__init__.py +7 -0
- fixcore/log/base.py +27 -0
- fixcore/log/factory.py +10 -0
- fixcore/log/file_log.py +70 -0
- fixcore/log/screen.py +32 -0
- fixcore/message/__init__.py +17 -0
- fixcore/message/cracker.py +243 -0
- fixcore/message/data_dictionary.py +298 -0
- fixcore/message/exceptions.py +21 -0
- fixcore/message/field.py +147 -0
- fixcore/message/message.py +403 -0
- fixcore/session/__init__.py +8 -0
- fixcore/session/session.py +532 -0
- fixcore/session/session_id.py +32 -0
- fixcore/session/session_settings.py +146 -0
- fixcore/session/state.py +60 -0
- fixcore/store/__init__.py +11 -0
- fixcore/store/base.py +49 -0
- fixcore/store/factory.py +33 -0
- fixcore/store/file_store.py +162 -0
- fixcore/store/memory.py +50 -0
- fixcore/transport/__init__.py +7 -0
- fixcore/transport/acceptor.py +166 -0
- fixcore/transport/framer.py +107 -0
- fixcore/transport/initiator.py +146 -0
- fixcore_engine-0.1.0.dist-info/METADATA +75 -0
- fixcore_engine-0.1.0.dist-info/RECORD +32 -0
- fixcore_engine-0.1.0.dist-info/WHEEL +5 -0
- fixcore_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
- fixcore_engine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""MessageFramer — splits a raw TCP byte stream into complete FIX messages.
|
|
2
|
+
|
|
3
|
+
FIX message structure on the wire::
|
|
4
|
+
|
|
5
|
+
8=FIX.4.2\\x01 ← BeginString (variable length)
|
|
6
|
+
9=<body_length>\\x01 ← BodyLength (variable length)
|
|
7
|
+
35=A\\x01... ← body (exactly body_length bytes)
|
|
8
|
+
10=NNN\\x01 ← CheckSum (always exactly 7 bytes: "10=" + 3 digits + SOH)
|
|
9
|
+
|
|
10
|
+
Framing algorithm:
|
|
11
|
+
1. Scan for ``8=`` to sync to the start of a message.
|
|
12
|
+
2. Parse ``9=<N>\\x01`` to read BodyLength.
|
|
13
|
+
3. Wait until the buffer holds prefix + N + 7 bytes.
|
|
14
|
+
4. Slice out the complete message and repeat.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
# CheckSum field is always "10=NNN\x01" — exactly 7 bytes.
|
|
20
|
+
_CHECKSUM_LEN = 7
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MessageFramer:
|
|
24
|
+
"""Stateful byte-stream framer for FIX messages.
|
|
25
|
+
|
|
26
|
+
Feed raw bytes in any chunk size; complete messages are returned.
|
|
27
|
+
|
|
28
|
+
Example::
|
|
29
|
+
|
|
30
|
+
framer = MessageFramer()
|
|
31
|
+
for raw_msg in framer.feed(socket_data):
|
|
32
|
+
process(raw_msg)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._buf: bytearray = bytearray()
|
|
37
|
+
|
|
38
|
+
def feed(self, data: bytes) -> list[bytes]:
|
|
39
|
+
"""Append *data* to the internal buffer and return all complete messages."""
|
|
40
|
+
self._buf.extend(data)
|
|
41
|
+
messages: list[bytes] = []
|
|
42
|
+
while True:
|
|
43
|
+
msg = self._try_extract()
|
|
44
|
+
if msg is None:
|
|
45
|
+
break
|
|
46
|
+
messages.append(msg)
|
|
47
|
+
return messages
|
|
48
|
+
|
|
49
|
+
def reset(self) -> None:
|
|
50
|
+
"""Discard all buffered bytes."""
|
|
51
|
+
self._buf.clear()
|
|
52
|
+
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
# Internal
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
def _try_extract(self) -> bytes | None:
|
|
58
|
+
buf = self._buf
|
|
59
|
+
|
|
60
|
+
# ── 1. Sync to BeginString ────────────────────────────────────
|
|
61
|
+
start = buf.find(b"8=")
|
|
62
|
+
if start == -1:
|
|
63
|
+
# Keep a trailing '8' — it may be the first byte of '8=' once more data arrives
|
|
64
|
+
if buf and buf[-1] == ord("8"):
|
|
65
|
+
del buf[:-1]
|
|
66
|
+
else:
|
|
67
|
+
buf.clear()
|
|
68
|
+
return None
|
|
69
|
+
if start > 0:
|
|
70
|
+
del buf[:start] # discard garbage before 8=
|
|
71
|
+
|
|
72
|
+
# ── 2. Find end of BeginString field ─────────────────────────
|
|
73
|
+
bs_soh = buf.find(b"\x01")
|
|
74
|
+
if bs_soh == -1:
|
|
75
|
+
return None # incomplete BeginString
|
|
76
|
+
|
|
77
|
+
# ── 3. Parse BodyLength field (must immediately follow) ───────
|
|
78
|
+
bl_start = bs_soh + 1
|
|
79
|
+
if len(buf) < bl_start + 3: # need at least "9=X"
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
if buf[bl_start : bl_start + 2] != b"9=":
|
|
83
|
+
# Malformed — skip past this "8=" and retry
|
|
84
|
+
del buf[:2]
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
bl_soh = buf.find(b"\x01", bl_start + 2)
|
|
88
|
+
if bl_soh == -1:
|
|
89
|
+
return None # incomplete BodyLength field
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
body_length = int(buf[bl_start + 2 : bl_soh])
|
|
93
|
+
except ValueError:
|
|
94
|
+
# Corrupt BodyLength — drop up to and including this SOH
|
|
95
|
+
del buf[: bl_soh + 1]
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
# ── 4. Wait for full message ──────────────────────────────────
|
|
99
|
+
body_start = bl_soh + 1
|
|
100
|
+
total = body_start + body_length + _CHECKSUM_LEN
|
|
101
|
+
|
|
102
|
+
if len(buf) < total:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
msg = bytes(buf[:total])
|
|
106
|
+
del buf[:total]
|
|
107
|
+
return msg
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""SocketInitiator — manages outbound FIX connections with auto-reconnect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from fixcore.application import Application
|
|
8
|
+
from fixcore.log.base import LogFactory
|
|
9
|
+
from fixcore.session.session import Session
|
|
10
|
+
from fixcore.session.session_id import SessionID
|
|
11
|
+
from fixcore.session.session_settings import SessionSettings
|
|
12
|
+
from fixcore.store.factory import StoreFactory
|
|
13
|
+
from fixcore.transport.framer import MessageFramer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SocketInitiator:
|
|
17
|
+
"""Dials outbound TCP connections for each configured initiator session.
|
|
18
|
+
|
|
19
|
+
Each session runs in its own :func:`asyncio.create_task` loop. On
|
|
20
|
+
disconnect the loop waits ``ReconnectInterval`` seconds (default 10) and
|
|
21
|
+
tries again until :meth:`stop` is called.
|
|
22
|
+
|
|
23
|
+
Usage::
|
|
24
|
+
|
|
25
|
+
initiator = SocketInitiator(settings, app, store_factory, log_factory)
|
|
26
|
+
async with initiator:
|
|
27
|
+
await asyncio.sleep(3600)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
settings: SessionSettings,
|
|
33
|
+
application: Application,
|
|
34
|
+
store_factory: StoreFactory,
|
|
35
|
+
log_factory: LogFactory,
|
|
36
|
+
) -> None:
|
|
37
|
+
self._settings = settings
|
|
38
|
+
self._app = application
|
|
39
|
+
self._store_factory = store_factory
|
|
40
|
+
self._log_factory = log_factory
|
|
41
|
+
|
|
42
|
+
self._sessions: dict[SessionID, Session] = {}
|
|
43
|
+
self._tasks: list[asyncio.Task[None]] = []
|
|
44
|
+
self._stop_event = asyncio.Event()
|
|
45
|
+
|
|
46
|
+
for sid in settings.session_ids:
|
|
47
|
+
conn_type = settings.get_or(sid, "ConnectionType", "initiator").lower()
|
|
48
|
+
if conn_type == "initiator":
|
|
49
|
+
self._sessions[sid] = Session(
|
|
50
|
+
sid, settings, application,
|
|
51
|
+
store_factory.create(sid),
|
|
52
|
+
log_factory.create(sid),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
# Lifecycle
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
async def start(self) -> None:
|
|
60
|
+
"""Launch a connect-loop task for each configured initiator session."""
|
|
61
|
+
self._stop_event.clear()
|
|
62
|
+
for sid, session in self._sessions.items():
|
|
63
|
+
task = asyncio.create_task(
|
|
64
|
+
self._connect_loop(sid, session),
|
|
65
|
+
name=f"initiator-{sid}",
|
|
66
|
+
)
|
|
67
|
+
self._tasks.append(task)
|
|
68
|
+
|
|
69
|
+
async def stop(self) -> None:
|
|
70
|
+
"""Signal all connect loops to exit and wait for them to finish."""
|
|
71
|
+
self._stop_event.set()
|
|
72
|
+
if self._tasks:
|
|
73
|
+
await asyncio.gather(*self._tasks, return_exceptions=True)
|
|
74
|
+
self._tasks.clear()
|
|
75
|
+
|
|
76
|
+
async def __aenter__(self) -> "SocketInitiator":
|
|
77
|
+
await self.start()
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
async def __aexit__(self, *_: object) -> None:
|
|
81
|
+
await self.stop()
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def sessions(self) -> dict[SessionID, Session]:
|
|
85
|
+
return dict(self._sessions)
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------------
|
|
88
|
+
# Per-session connect loop
|
|
89
|
+
# ------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
async def _connect_loop(self, sid: SessionID, session: Session) -> None:
|
|
92
|
+
host = self._settings.get(sid, "SocketConnectHost")
|
|
93
|
+
port = int(self._settings.get(sid, "SocketConnectPort"))
|
|
94
|
+
reconnect = self._settings.get_int(sid, "ReconnectInterval", 10)
|
|
95
|
+
|
|
96
|
+
while not self._stop_event.is_set():
|
|
97
|
+
try:
|
|
98
|
+
reader, writer = await asyncio.open_connection(host, port)
|
|
99
|
+
except OSError:
|
|
100
|
+
# Connection refused or network error — wait and retry
|
|
101
|
+
await self._interruptible_sleep(reconnect)
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
framer = MessageFramer()
|
|
105
|
+
|
|
106
|
+
async def send_fn(data: bytes, _w: asyncio.StreamWriter = writer) -> None:
|
|
107
|
+
_w.write(data)
|
|
108
|
+
await _w.drain()
|
|
109
|
+
|
|
110
|
+
await session.on_connect(send_fn)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
while not self._stop_event.is_set():
|
|
114
|
+
try:
|
|
115
|
+
data = await asyncio.wait_for(reader.read(4096), timeout=1.0)
|
|
116
|
+
except asyncio.TimeoutError:
|
|
117
|
+
continue
|
|
118
|
+
except (ConnectionResetError, asyncio.IncompleteReadError):
|
|
119
|
+
break
|
|
120
|
+
if not data:
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
for raw_msg in framer.feed(data):
|
|
124
|
+
await session.on_data(raw_msg)
|
|
125
|
+
|
|
126
|
+
finally:
|
|
127
|
+
await session.on_disconnect()
|
|
128
|
+
try:
|
|
129
|
+
writer.close()
|
|
130
|
+
await writer.wait_closed()
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
framer.reset()
|
|
134
|
+
|
|
135
|
+
if not self._stop_event.is_set():
|
|
136
|
+
await self._interruptible_sleep(reconnect)
|
|
137
|
+
|
|
138
|
+
async def _interruptible_sleep(self, seconds: float) -> None:
|
|
139
|
+
"""Sleep for *seconds* but wake immediately if stop is requested."""
|
|
140
|
+
try:
|
|
141
|
+
await asyncio.wait_for(
|
|
142
|
+
asyncio.shield(self._stop_event.wait()),
|
|
143
|
+
timeout=seconds,
|
|
144
|
+
)
|
|
145
|
+
except asyncio.TimeoutError:
|
|
146
|
+
pass
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fixcore-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure Python FIX protocol engine
|
|
5
|
+
Author-email: Aidan Chisholm <aidan.chisholm@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Aidan Alexander Chisholm
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/aidan-chisholm/fixcore
|
|
29
|
+
Project-URL: Repository, https://github.com/aidan-chisholm/fixcore
|
|
30
|
+
Keywords: fix,financial,protocol,trading,quickfix
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Requires-Python: >=3.11
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
License-File: LICENSE
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
42
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
43
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
44
|
+
Provides-Extra: gui
|
|
45
|
+
Requires-Dist: aiohttp>=3.9; extra == "gui"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
<img src="fixcore_logo.svg" alt="FIXcore" width="340" />
|
|
49
|
+
|
|
50
|
+
Pure Python FIX protocol engine — mirrors the QuickFIX architecture (QuickFIX/n, QuickFIX/J).
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
- Full FIX 4.2 session layer (Logon, Logout, Heartbeat, ResendRequest, SequenceReset, Reject)
|
|
55
|
+
- Message encoding/decoding with DataDictionary validation
|
|
56
|
+
- Repeating groups
|
|
57
|
+
- Async transport: `SocketAcceptor` + `SocketInitiator` with auto-reconnect
|
|
58
|
+
- `FileStore` and `MemoryStore` persistence
|
|
59
|
+
- `MessageCracker` dispatch mixin
|
|
60
|
+
- Lightweight browser GUI via aiohttp (`tools/fix_gui.py`) — session management, message builder, live message log
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install fixcore-engine
|
|
66
|
+
pip install "fixcore-engine[gui]" # includes aiohttp for the GUI
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install -e ".[dev,gui]"
|
|
73
|
+
pytest
|
|
74
|
+
python tools/fix_gui.py
|
|
75
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
fixcore/__init__.py,sha256=kB3VWakZe8vfRllMdcPwFI7cIEnLKeVQKccgwvyjLuI,189
|
|
2
|
+
fixcore/application.py,sha256=NF3bZ9e61PVTkH6MecMJHPY85Q26blxkq7eJ0zBis0Q,1658
|
|
3
|
+
fixcore/log/__init__.py,sha256=LhtRdV8djjICrNRha-_hvWO44P8-SEJLGaBazfSTOnc,316
|
|
4
|
+
fixcore/log/base.py,sha256=Tuyre4h2Cngw_hloB0749jRCMRKhLxF-HHbv56sHCJQ,695
|
|
5
|
+
fixcore/log/factory.py,sha256=qYMSkel0w251ZWiBbtHiqpxejWmi1vfqXmw7CtSzM4A,384
|
|
6
|
+
fixcore/log/file_log.py,sha256=6jaL--3idj8mjahnOi_kz4cEc9GTt1fgKDaiNy5YRD8,2177
|
|
7
|
+
fixcore/log/screen.py,sha256=jQ6HsGpoYPNY1duASqosl6js5dQXX07Zm_f1bFTySMI,931
|
|
8
|
+
fixcore/message/__init__.py,sha256=ITFr0KySajGvyMfCxVx430MUQSbDaCSWEhmVAv7kfCY,708
|
|
9
|
+
fixcore/message/cracker.py,sha256=97xdfUeLJV2PmbDn4HJQVYbW5Zn4A5dGfwgI9jCsKs0,9160
|
|
10
|
+
fixcore/message/data_dictionary.py,sha256=at1Pc0Ku7j2LMw2FEp7GA428v3aYCr88WzbxDy456Xs,11270
|
|
11
|
+
fixcore/message/exceptions.py,sha256=SzjVpX0gs79lVeN5UdLhzSawBhph4T-GZx8S2jkkx_c,629
|
|
12
|
+
fixcore/message/field.py,sha256=M9oi9upU5douqaG5iCxZDQB3SmOzzaIEqEMpxbN0Bgc,5018
|
|
13
|
+
fixcore/message/message.py,sha256=pZNMSBbxKYON19CIq6gvlK4SzMVHcs7FPDZXVEfjqOE,13610
|
|
14
|
+
fixcore/session/__init__.py,sha256=cVkPLJ_DzB3A7Z8ssQb7-FAr09n8_mnwdCHM4C7iMxI,344
|
|
15
|
+
fixcore/session/session.py,sha256=LE2O7qOka5A8zUv8smoGgD0NEx5HsWta5kr42GpVSeY,20317
|
|
16
|
+
fixcore/session/session_id.py,sha256=ykKDV38P6RRnC1uMDdKGKoFNLwOCl8rfjRPo5x444mA,880
|
|
17
|
+
fixcore/session/session_settings.py,sha256=epx6aflPKrwSwL3CswnRmqqrLzWqdsxCYgH8z0EzzsU,5032
|
|
18
|
+
fixcore/session/state.py,sha256=gazczlRCpSibNHFVxY_yiQRCzIMxxFf_V9j2OPP_K4Y,1537
|
|
19
|
+
fixcore/store/__init__.py,sha256=co8EEl5aKMVI_9MHpsM8Gmhr_yIJA5xWu6cJ3-7TD68,431
|
|
20
|
+
fixcore/store/base.py,sha256=nrpDlrU9L_htCpwpnNMhKJAM04FPZ0yjkWdwGOkdcMA,1577
|
|
21
|
+
fixcore/store/factory.py,sha256=iZvAiITaZrwg2-SJjjc7r2_m7CNsdmc_8Ama_8CpVJg,990
|
|
22
|
+
fixcore/store/file_store.py,sha256=Ih_15nohYDq5dI6BtO7cs87Bt1VpwVzTwfP_zkVP8GI,5469
|
|
23
|
+
fixcore/store/memory.py,sha256=AkGsjXh1oTM-CXZEzsNpQWZXMRQ485GgXdRupPG-08U,1421
|
|
24
|
+
fixcore/transport/__init__.py,sha256=EuZg6qPwWOMuoFPcgvBjDX2bFYsi5QjhpEwNa87oIPk,296
|
|
25
|
+
fixcore/transport/acceptor.py,sha256=4fnxGaZdzHAgfS_L3Wku729xGhczRd3oizrz9pEsy1U,5933
|
|
26
|
+
fixcore/transport/framer.py,sha256=tReh1gLDtX8yv3wlNO4Y1CTMx91WpseuUPxMnkSU4_o,3643
|
|
27
|
+
fixcore/transport/initiator.py,sha256=mwRa4QRrCEDHkoCzpWrByIYAf4JjF9EbZK6HZnxnIf4,5169
|
|
28
|
+
fixcore_engine-0.1.0.dist-info/licenses/LICENSE,sha256=CZx7yTfXMKsRzuWtSQetjP_1vrKdPY2AJIVbRA7H9eM,1081
|
|
29
|
+
fixcore_engine-0.1.0.dist-info/METADATA,sha256=4upgAZxM_JdNPqSGI0OhRCMrRKM5SEO15oZekedkNRA,3028
|
|
30
|
+
fixcore_engine-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
31
|
+
fixcore_engine-0.1.0.dist-info/top_level.txt,sha256=0K_SIcbEjG9Y6KafA3APKFq5S4ZizMkDmycBA41Lchk,8
|
|
32
|
+
fixcore_engine-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aidan Alexander Chisholm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fixcore
|