mainframe-mocker 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.
- mainframe_mocker-0.1.0.dist-info/METADATA +504 -0
- mainframe_mocker-0.1.0.dist-info/RECORD +66 -0
- mainframe_mocker-0.1.0.dist-info/WHEEL +4 -0
- mainframe_mocker-0.1.0.dist-info/entry_points.txt +6 -0
- mainframe_mocker-0.1.0.dist-info/licenses/LICENSE +21 -0
- mfmock/__init__.py +44 -0
- mfmock/__main__.py +76 -0
- mfmock/cli.py +78 -0
- mfmock/core/__init__.py +1 -0
- mfmock/core/capture.py +311 -0
- mfmock/core/cli.py +154 -0
- mfmock/core/ebcdic.py +24 -0
- mfmock/core/keys.py +83 -0
- mfmock/core/preview.py +11 -0
- mfmock/core/protocol.py +44 -0
- mfmock/core/server.py +164 -0
- mfmock/core/telnet.py +362 -0
- mfmock/core/terminal.py +33 -0
- mfmock/core/testing/__init__.py +11 -0
- mfmock/core/testing/emulator.py +46 -0
- mfmock/core/testing/replay.py +150 -0
- mfmock/core/testing/server.py +49 -0
- mfmock/core/testing/transport.py +178 -0
- mfmock/core/testing/virtual_terminal.py +219 -0
- mfmock/core/ui/__init__.py +20 -0
- mfmock/core/ui/app.py +158 -0
- mfmock/core/ui/conversation.py +188 -0
- mfmock/core/ui/frame.py +83 -0
- mfmock/core/ui/layout.py +202 -0
- mfmock/core/ui/messages.py +129 -0
- mfmock/core/ui/results.py +53 -0
- mfmock/core/ui/screen.py +144 -0
- mfmock/core/ui/style.py +105 -0
- mfmock/core/ui/template.py +452 -0
- mfmock/core/ui/validators.py +50 -0
- mfmock/core/ui/widgets.py +168 -0
- mfmock/demo/__init__.py +165 -0
- mfmock/demo/templates/fields.j2 +11 -0
- mfmock/demo/templates/hello.j2 +3 -0
- mfmock/demo/templates/signon.j2 +6 -0
- mfmock/py.typed +0 -0
- mfmock/pytest_plugin.py +153 -0
- mfmock/templates/goodbye.j2 +3 -0
- mfmock/templates/help.j2 +5 -0
- mfmock/templates/list.j2 +3 -0
- mfmock/templates/menu.j2 +3 -0
- mfmock/templates/widgets.j2 +30 -0
- mfmock/tn3270/__init__.py +7 -0
- mfmock/tn3270/adapter.py +60 -0
- mfmock/tn3270/cli.py +17 -0
- mfmock/tn3270/datastream.py +312 -0
- mfmock/tn3270/emulator.py +269 -0
- mfmock/tn3270/negotiation.py +145 -0
- mfmock/tn3270/renderer.py +123 -0
- mfmock/tn3270/s3270.py +121 -0
- mfmock/tn3270/terminal.py +31 -0
- mfmock/tn3270/testing.py +63 -0
- mfmock/tn5250/__init__.py +7 -0
- mfmock/tn5250/adapter.py +37 -0
- mfmock/tn5250/cli.py +28 -0
- mfmock/tn5250/datastream.py +258 -0
- mfmock/tn5250/emulator.py +301 -0
- mfmock/tn5250/negotiation.py +103 -0
- mfmock/tn5250/renderer.py +153 -0
- mfmock/tn5250/terminal.py +36 -0
- mfmock/tn5250/testing.py +50 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mainframe-mocker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build mock IBM mainframe terminal applications (TN3270 and TN5250) and test terminal clients against them
|
|
5
|
+
Keywords: 3270,tn3270,5250,tn5250,mainframe,mock,terminal,emulator,testing,cics,ibm-i
|
|
6
|
+
Author: Ben Goldsmith
|
|
7
|
+
Author-email: Ben Goldsmith <ben.goldsmith@clearroute.io>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: Pytest
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing :: Mocking
|
|
20
|
+
Classifier: Topic :: Terminals :: Telnet
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: jinja2>=3.1
|
|
23
|
+
Requires-Python: >=3.13
|
|
24
|
+
Project-URL: Homepage, https://github.com/bengieeee/mainframe-mocker
|
|
25
|
+
Project-URL: Documentation, https://github.com/bengieeee/mainframe-mocker#readme
|
|
26
|
+
Project-URL: Source, https://github.com/bengieeee/mainframe-mocker
|
|
27
|
+
Project-URL: Issues, https://github.com/bengieeee/mainframe-mocker/issues
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# mainframe-mocker
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/mainframe-mocker/)
|
|
33
|
+
[](https://pypi.org/project/mainframe-mocker/)
|
|
34
|
+
[](https://github.com/bengieeee/mainframe-mocker/actions/workflows/publish.yml)
|
|
35
|
+
[](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/LICENSE)
|
|
36
|
+
|
|
37
|
+
**Mock IBM mainframe green-screen hosts in plain Python.** You write the screens; `mfmock`
|
|
38
|
+
speaks the wire protocol. Real terminal emulators (x3270, c3270, s3270, tn5250, Reflection,
|
|
39
|
+
ZOC...), RPA bots and screen-scraping libraries connect to your mock as if it were a CICS,
|
|
40
|
+
TSO or IBM i host.
|
|
41
|
+
|
|
42
|
+
Use it to:
|
|
43
|
+
|
|
44
|
+
- **test terminal emulators and 3270/5250 client libraries** against a host you control, with
|
|
45
|
+
every screen size and colour model, TN3270E on or off, and TLS;
|
|
46
|
+
- **run integration and E2E tests** for RPA bots and screen-scraping code in CI, with no
|
|
47
|
+
mainframe, no VPN and no shared test region;
|
|
48
|
+
- **rebuild a real host flow from a recorded session** and check the mock against that
|
|
49
|
+
recording;
|
|
50
|
+
- **demo and train** on a mainframe flow that anybody can run on a laptop.
|
|
51
|
+
|
|
52
|
+
Features:
|
|
53
|
+
|
|
54
|
+
- **TN3270 / TN3270E** (models 2-5, mono and colour, TLS) and **TN5250** (IBM i, 24x80 and
|
|
55
|
+
27x132, mono and colour, TLS). The same app serves both.
|
|
56
|
+
- **Screens are Jinja2 pictures:** a template looks like the screen, and Python keeps the
|
|
57
|
+
behaviour.
|
|
58
|
+
- **A test kit:**
|
|
59
|
+
- an in-memory virtual terminal that speaks real protocol bytes;
|
|
60
|
+
- a TCP client and an ephemeral-port server;
|
|
61
|
+
- a pytest plugin that runs each test over both protocols;
|
|
62
|
+
- a driver for the real `s3270` emulator;
|
|
63
|
+
- replay of recorded sessions.
|
|
64
|
+
- **Small footprint:** pure Python 3.13+, needing only the standard library and Jinja2.
|
|
65
|
+
|
|
66
|
+
## Contents
|
|
67
|
+
|
|
68
|
+
- [Install](#install)
|
|
69
|
+
- [Try it in 30 seconds](#try-it-in-30-seconds)
|
|
70
|
+
- [A mock in five minutes](#a-mock-in-five-minutes)
|
|
71
|
+
- [Testing with mfmock](#testing-with-mfmock)
|
|
72
|
+
- [End-to-end testing of real clients](#end-to-end-testing-of-real-clients)
|
|
73
|
+
- [Concepts](#concepts)
|
|
74
|
+
- [Screen templates](#screen-templates)
|
|
75
|
+
- [Mocks from recordings](#mocks-from-recordings)
|
|
76
|
+
- [Command line and environment](#command-line-and-environment)
|
|
77
|
+
- [Protocols](#protocols)
|
|
78
|
+
- [Architecture](#architecture)
|
|
79
|
+
- [For AI agents and coding tools](#for-ai-agents-and-coding-tools)
|
|
80
|
+
- [Roadmap](#roadmap)
|
|
81
|
+
- [Development](#development)
|
|
82
|
+
|
|
83
|
+
More detail is in [`docs/`](https://github.com/bengieeee/mainframe-mocker/tree/HEAD/docs/): [templates](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/templates.md), [testing](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/testing.md),
|
|
84
|
+
[recordings](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/recordings.md), [protocols](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/protocols.md) and the [API](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/api.md).
|
|
85
|
+
|
|
86
|
+
## Install
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
pip install mainframe-mocker # or: uv add mainframe-mocker
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The distribution is `mainframe-mocker`, the import package is `mfmock`, and the command is
|
|
93
|
+
`mfmock`. Installing it also registers a pytest plugin (see [Testing](#testing-with-mfmock)).
|
|
94
|
+
|
|
95
|
+
## Try it in 30 seconds
|
|
96
|
+
|
|
97
|
+
`mfmock demo` serves a small built-in host (sign-on, menu, a form, a paged list and a field
|
|
98
|
+
showcase):
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
mfmock demo # TN3270 on port 3270
|
|
102
|
+
c3270 localhost:3270 # sign on as DEMO / DEMO
|
|
103
|
+
|
|
104
|
+
mfmock demo --protocol 5250 # TN5250 on port 5250
|
|
105
|
+
tn5250 localhost:5250
|
|
106
|
+
|
|
107
|
+
mfmock demo --port 2323 --users alice:secret,bob:hunter2 --trace --log-level DEBUG
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The demo has these screens and transaction codes:
|
|
111
|
+
|
|
112
|
+
| Screen | Transaction | Shows |
|
|
113
|
+
| -------------- | ---------------- | ------------------------------------------------------------------ |
|
|
114
|
+
| Sign-on | | an input field, a hidden password field, error messages |
|
|
115
|
+
| Main menu | `MENU` | a numbered menu |
|
|
116
|
+
| Greeting | `HELO` | a form and a message |
|
|
117
|
+
| Guest book | `LIST` | a paged list (PF7 and PF8) |
|
|
118
|
+
| Field showcase | `FLDS` | every colour, highlight, numeric field, hidden field and validator |
|
|
119
|
+
| Help, sign-off | `PF1`, `SIGNOFF` | |
|
|
120
|
+
|
|
121
|
+
Point an emulator, a bot or an E2E suite at it before you write a mock of your own.
|
|
122
|
+
|
|
123
|
+
## A mock in five minutes
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# hello.py
|
|
127
|
+
from mfmock import Application, Exit, MessageCatalog, Screen, Stay, msg, run
|
|
128
|
+
|
|
129
|
+
app = Application(
|
|
130
|
+
"HELLO",
|
|
131
|
+
start="MAIN",
|
|
132
|
+
catalog=MessageCatalog(prefix="HEL", messages={"GREETED": ("HEL0100I", "HELLO {name}")}),
|
|
133
|
+
pf_legend="ENTER=Greet PF3=Exit",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.screen
|
|
138
|
+
class Main(Screen):
|
|
139
|
+
name = tran = "MAIN"
|
|
140
|
+
title = "HELLO MAINFRAME"
|
|
141
|
+
|
|
142
|
+
template_source = """\
|
|
143
|
+
{{ row(5) }}{{ prompt(col=2) }}YOUR NAME . . : {{ input('name', 20, label='NAME', validators=[required]) }}
|
|
144
|
+
{{ row(7) }}{{ prompt(col=2) }}GREETING . . : {{ output('greeting', 50) }}
|
|
145
|
+
""" # or template = "main.j2", in a templates/ directory
|
|
146
|
+
|
|
147
|
+
def enter(self): # plain sync code; values are upper-cased text
|
|
148
|
+
self.values["greeting"] = f"HELLO, {self.v('name')}!"
|
|
149
|
+
return Stay(msg("GREETED", name=self.v("name")))
|
|
150
|
+
|
|
151
|
+
def pf(self, n):
|
|
152
|
+
return Exit() if n == 3 else None # None falls through to the app's PF defaults
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == "__main__":
|
|
156
|
+
run(app, protocols=("tn3270", "tn5250")) # --protocol, --port, --trace, --tls-*, env vars
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
python hello.py --port 3270 & # then: c3270 localhost:3270
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The frame adds the header (system ID, title, user, clock), the `TRAN:` command line, the
|
|
164
|
+
message line and the key legend. This is [`examples/hello.py`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/examples/hello.py); its tests
|
|
165
|
+
are in [`examples/test_hello.py`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/examples/test_hello.py).
|
|
166
|
+
|
|
167
|
+
## Testing with mfmock
|
|
168
|
+
|
|
169
|
+
### The pytest plugin
|
|
170
|
+
|
|
171
|
+
Installing `mainframe-mocker` registers a pytest plugin. Its fixtures run each test once per
|
|
172
|
+
protocol:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from hello import app
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_greets(mf_terminal): # runs as test_greets[3270] and test_greets[5250]
|
|
179
|
+
t = mf_terminal(app) # an in-memory terminal: real protocol bytes, no sockets
|
|
180
|
+
t.enter(your_name="ada") # fill the field captioned "YOUR NAME", press Enter
|
|
181
|
+
assert t.message == "HEL0100I HELLO ADA"
|
|
182
|
+
assert "HELLO, ADA!" in t.text()
|
|
183
|
+
t.pf(3)
|
|
184
|
+
assert t.closed
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_over_tcp(mf_server):
|
|
188
|
+
server = mf_server(app) # listening on 127.0.0.1:<free port> for this test
|
|
189
|
+
t = server.connect() # a TCP client with full telnet negotiation
|
|
190
|
+
assert "HELLO MAINFRAME" in t.row(1)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
| Fixture, option or marker | What it does |
|
|
194
|
+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
195
|
+
| `mf_terminal(app, terminal_type=None, **kw)` | opens an in-memory `VirtualTerminal`. The default terminal is 24x80 colour (`IBM-3279-2-E` or `IBM-3179-2`). `kw` takes `device_name=` and `peer=`. |
|
|
196
|
+
| `mf_server(app, config=None)` | serves the app on TCP. Returns an `MfServer` with `host`, `port`, `address` (`"host:port"`) and `connect(terminal_type=None)`. |
|
|
197
|
+
| `mf_protocol` | `"tn3270"` or `"tn5250"`, the protocol of this run |
|
|
198
|
+
| `--mf-protocol 3270` | chooses the protocols for the run: `3270`, `5250` or `3270,5250` (the default) |
|
|
199
|
+
| `@pytest.mark.mf_protocols("5250")` | limits one test to some protocols |
|
|
200
|
+
|
|
201
|
+
Terminals and servers are closed when the test ends.
|
|
202
|
+
|
|
203
|
+
### The VirtualTerminal API
|
|
204
|
+
|
|
205
|
+
| Read the screen | Act |
|
|
206
|
+
| ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
|
|
207
|
+
| `text()`, `lines()`, `row(n)` (1-based) | `fill(target, value)` |
|
|
208
|
+
| `find(text)` gives `(row, col)` or `None` | `enter(**fill)`: keyword names become captions (`your_name` matches `YOUR NAME`) |
|
|
209
|
+
| `assert_text(*texts)` | `press(Key.PF5, **fill)`, `pf(n)`, `pa(n)`, `clear()` |
|
|
210
|
+
| `message`: the message line | `type(text)`, `move_cursor(row, col)` |
|
|
211
|
+
| `alarm`: whether the last screen sounded the alarm | `tran(code)`: type a transaction code and press Enter |
|
|
212
|
+
| `cursor`, `rows`, `cols`, `closed` | `close()` |
|
|
213
|
+
| `fields`, `all_fields`, `field(target)` give a `FieldView` (`value`, `hidden`, `numeric`, `protected`, `color`...) | |
|
|
214
|
+
|
|
215
|
+
A `target` is a caption on the screen (the first input field after it on the same row), a
|
|
216
|
+
1-based `(row, col)` inside the field, or the field's index.
|
|
217
|
+
|
|
218
|
+
### Without pytest
|
|
219
|
+
|
|
220
|
+
The same terminals come from the protocol packages:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from mfmock import ServerConfig
|
|
224
|
+
from mfmock.core.testing import running_server
|
|
225
|
+
from mfmock.tn3270 import TN3270Adapter
|
|
226
|
+
from mfmock.tn3270.testing import connect, virtual_terminal
|
|
227
|
+
|
|
228
|
+
from hello import app
|
|
229
|
+
|
|
230
|
+
with virtual_terminal(app, "IBM-3278-5") as t: # 27x132; any 3278/3279 model works
|
|
231
|
+
t.enter(your_name="grace")
|
|
232
|
+
assert "HELLO, GRACE!" in t.text()
|
|
233
|
+
|
|
234
|
+
config = ServerConfig(host="127.0.0.1", port=0) # port 0: any free port
|
|
235
|
+
with running_server(TN3270Adapter(), app, config) as server:
|
|
236
|
+
with connect("127.0.0.1", server.port, "IBM-3279-2-E") as t:
|
|
237
|
+
assert "HELLO MAINFRAME" in t.row(1)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`mfmock.tn5250.testing` has the same `virtual_terminal` and `connect`. Its default terminal
|
|
241
|
+
is `IBM-3179-2`, and `connect` takes `device_name=`.
|
|
242
|
+
|
|
243
|
+
## End-to-end testing of real clients
|
|
244
|
+
|
|
245
|
+
The mock is a real TN3270 or TN5250 server, so any client can be tested against it: an
|
|
246
|
+
emulator, an RPA bot, a `py3270` or `tnz` script, or your own client library.
|
|
247
|
+
|
|
248
|
+
**From pytest:** serve the app with `mf_server` and give the client `server.host` and
|
|
249
|
+
`server.port`. The built-in driver for the real `s3270` emulator shows the pattern:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
import pytest
|
|
253
|
+
|
|
254
|
+
from mfmock.demo import create_app
|
|
255
|
+
from mfmock.tn3270.s3270 import S3270
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@pytest.mark.mf_protocols("3270")
|
|
259
|
+
@pytest.mark.skipif(not S3270.available(), reason="s3270 not installed")
|
|
260
|
+
def test_real_emulator_signs_on(mf_server):
|
|
261
|
+
server = mf_server(create_app())
|
|
262
|
+
emulator = S3270() # or your client under test
|
|
263
|
+
try:
|
|
264
|
+
emulator.connect(server.host, server.port)
|
|
265
|
+
emulator.type("demo")
|
|
266
|
+
emulator.cmd("Tab()")
|
|
267
|
+
emulator.type("demo")
|
|
268
|
+
assert "MAIN MENU" in emulator.enter()
|
|
269
|
+
finally:
|
|
270
|
+
emulator.close()
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**From any language or CI system:** run the host as a process or a service:
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
mfmock demo --host 127.0.0.1 --port 3270 & # or: python my_mock.py --port 3270 &
|
|
277
|
+
MFMOCK_PORT=3270 MFMOCK_PROTOCOL=5250 mfmock demo
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
# GitHub Actions: a mock host for the job's E2E tests
|
|
282
|
+
- run: pip install mainframe-mocker
|
|
283
|
+
- run: mfmock demo --port 3270 &
|
|
284
|
+
- run: npm run test:e2e # your client connects to localhost:3270
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**What to test for:**
|
|
288
|
+
|
|
289
|
+
- Screen sizes and colour models. Pass a terminal type:
|
|
290
|
+
- 3270: `IBM-3278-2` to `-5` (mono), `IBM-3279-2` to `-5` (colour), `-E` for extended
|
|
291
|
+
attributes, and `IBM-DYNAMIC` (the size comes from the Query Reply);
|
|
292
|
+
- 5250: `IBM-3179-2`, `IBM-3477-FC` (27x132) and others.
|
|
293
|
+
- `--no-tn3270e` for clients that should fall back to plain TN3270.
|
|
294
|
+
- TLS: `--tls-port 992 --tls-cert cert.pem --tls-key key.pem`.
|
|
295
|
+
- Idle timeouts: `--idle-timeout 30`.
|
|
296
|
+
- Errors and the alarm: send an invalid entry and an unknown transaction.
|
|
297
|
+
- Disconnects: `Exit()` closes the session.
|
|
298
|
+
- `--trace` hex-dumps every record, to debug a client byte by byte.
|
|
299
|
+
- Each connection gets a device or LU name (`ServerConfig(device_prefix=...)` or
|
|
300
|
+
`device_names=`). Its logs are `key=value` lines tagged `lu=...`.
|
|
301
|
+
|
|
302
|
+
## Concepts
|
|
303
|
+
|
|
304
|
+
| Concept | In short |
|
|
305
|
+
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
306
|
+
| `Application(name, start=, catalog=, frame=, theme=, templates=, idle_timeout=, signoff_trans=, default_access=, pf_legend=, ...)` | the host. `@app.screen` registers a screen and its transaction code. Subclass it to override hooks. |
|
|
307
|
+
| `Screen` | one screen. Class attributes: `name`, `tran`, `title`, `access`, `help_text`, `pf_legend`, `default_cursor`, `template` or `template_source`. |
|
|
308
|
+
| Handlers | `setup()`, `refresh()`, `resume()`, `enter()`, `pf(n)`, `scroll(delta)`, `clear()`, `pa(n)`. Each returns a Result, or `None` for the app's default behaviour. |
|
|
309
|
+
| Results | `Stay(msg, cursor=)`, `Goto(name, args)`, `Back()`, `Home()`, `Restart()` (sign off) and `Exit()` (disconnect) |
|
|
310
|
+
| `self.values`, `self.v(name)`, `self.state` | the screen's field values (typed text is upper-cased unless `upper=False`), one value (`""` if unset), the per-connection state |
|
|
311
|
+
| Errors | `raise ScreenError("REQUIRED", cursor="name", field="NAME")` from anywhere. Other exceptions show `...9999E UNEXPECTED ERROR - REFERENCE <ref>` and are logged with the reference. |
|
|
312
|
+
| `MessageCatalog(prefix, messages)` | message IDs `{prefix}nnnn{I,W,E}`, with library defaults (`REQUIRED`, `INVALID_FIELD`, `TRAN_UNKNOWN`, `NOT_AUTHORISED`, `TOP`, `BOTTOM`...) that you can override by ID. Use `msg("ID", **params)`. |
|
|
313
|
+
| Validators | `required`, `digits(n=None)`, `one_of(*options)`, `matches(regex)`, or any `(value, field) -> MessageRef \| None` |
|
|
314
|
+
| Widgets | `MenuScreen` (`options`), `ListScreen` (`fetch(offset, limit)`, paging, a selection column), `HelpScreen`, `GoodbyeScreen` |
|
|
315
|
+
| Frame and theme | `StandardFrame(sysid=...)` draws the chrome, and `PlainFrame` draws none. A `Theme` maps semantic styles (`label`, `input`, `msg_error`...) to a `Style(intensity, color, highlight)`, which degrades on mono terminals. |
|
|
316
|
+
| App hooks | `create_state(conv)`, `authorize(state, requirement)`, `on_restart(conv)`, `on_end(conv)`, `user_label(conv)`, `clock()` |
|
|
317
|
+
| Below the UI | any object with `async handle(session)` can be served. Use `session.send(bytes)`, `await session.receive(timeout)` and `session.terminal`. |
|
|
318
|
+
|
|
319
|
+
[`src/mfmock/demo`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/src/mfmock/demo/__init__.py) is a compact example of most of these.
|
|
320
|
+
|
|
321
|
+
## Screen templates
|
|
322
|
+
|
|
323
|
+
A template is a Jinja2 picture of the screen body: each rendered line is a row, and tags mark
|
|
324
|
+
where protected text and fields begin.
|
|
325
|
+
|
|
326
|
+
```jinja
|
|
327
|
+
{{ row(3) }}{{ center('*** SIGN ON ***') }}
|
|
328
|
+
{{ row(6) }}{{ prompt(col=17) }}USER ID . . . . :{{ input('userid', 8, col=36, validators=[required]) }}
|
|
329
|
+
{{ prompt(col=17) }}PASSWORD . . . . :{{ input('password', 8, col=36, hidden=True) }}
|
|
330
|
+
{% for i in range(rows - 20) %}
|
|
331
|
+
{{ row(10 + i) }}{{ output('line_' ~ i, cols - 2, col=2) }}
|
|
332
|
+
{% endfor %}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
| Tag | Meaning |
|
|
336
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
337
|
+
| `row(n)` | at the start of a line: this line is row `n` |
|
|
338
|
+
| `text(style='label', col=, length=)` | protected text up to the next tag |
|
|
339
|
+
| `prompt(...)` | a `text` that captions the next field, which is also the field's label in messages |
|
|
340
|
+
| `input(name, length, col=, ...)`, `output(...)` | a field. Options: `style`, `numeric`, `hidden`, `upper`, `align`, `initial`, `label`, `validators`, `wrap`, `end`. |
|
|
341
|
+
| `center(text, style='title')` | centred text |
|
|
342
|
+
|
|
343
|
+
- Templates see `rows`, `cols`, `screen`, the validators, `ljust` and `rjust`. One template
|
|
344
|
+
serves every screen size, and is compiled once per size.
|
|
345
|
+
- Mistakes raise `TemplateLayoutError` naming the template, row and column. Examples are an
|
|
346
|
+
overlap, a field off the screen, or text without a tag.
|
|
347
|
+
- Preview a template, with a column ruler and its fields:
|
|
348
|
+
|
|
349
|
+
```sh
|
|
350
|
+
mfmock preview templates/signon.j2 --size 27x132 --set user='"ADA"'
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The library's own templates are `mfmock/menu.j2`, `list.j2`, `help.j2` and `goodbye.j2`, with
|
|
354
|
+
macros in `widgets.j2`. See [docs/templates.md](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/templates.md).
|
|
355
|
+
|
|
356
|
+
## Mocks from recordings
|
|
357
|
+
|
|
358
|
+
To mock a real host, record a session with an emulator that saves an HTML session (capture)
|
|
359
|
+
report. Then rebuild the host screen by screen:
|
|
360
|
+
|
|
361
|
+
```sh
|
|
362
|
+
mfmock capture flow.html # every screen, entry and key, numbered
|
|
363
|
+
mfmock capture flow.html --code 18 # a starting template for screen #18
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Check the mock against the recording. `replay` types the recorded data, presses the recorded
|
|
367
|
+
keys and compares every screen: text, cursor and the field map.
|
|
368
|
+
|
|
369
|
+
<!-- readme-test: compile-only (needs a recording) -->
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from mfmock.core.capture import load_capture
|
|
373
|
+
from mfmock.core.testing import mask_row, replay
|
|
374
|
+
from mfmock.tn3270.testing import virtual_terminal
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def test_matches_recording():
|
|
378
|
+
with virtual_terminal(app, device_name="TCPD00HK") as t:
|
|
379
|
+
replay(t, load_capture("tests/captures/flow.html"),
|
|
380
|
+
variables={"password": "SECRET"}, # values the recording hides
|
|
381
|
+
masks=[r"\d\d:\d\d:\d\d", mask_row(11, 51)]) # clocks, client IP
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The first screen that differs raises `ReplayMismatch`, which lists the differences. See
|
|
385
|
+
[docs/recordings.md](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/recordings.md).
|
|
386
|
+
|
|
387
|
+
## Command line and environment
|
|
388
|
+
|
|
389
|
+
`mfmock.run(app, argv=None, protocols=None, prog=, description=, env_prefix="MFMOCK_", config=ServerConfig(...), add_arguments=)`
|
|
390
|
+
gives every mock these flags. `mfmock demo` has them too.
|
|
391
|
+
|
|
392
|
+
| Flag | Environment variable | Default |
|
|
393
|
+
| ------------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------- |
|
|
394
|
+
| `--protocol {3270,5250}` (when there are several `protocols`) | `<PREFIX>PROTOCOL` | the first protocol |
|
|
395
|
+
| `--host`, `--port` | `<PREFIX>HOST`, `<PREFIX>PORT` | `0.0.0.0`; `3270` (`5250` for TN5250 when it is not the first protocol) |
|
|
396
|
+
| `--log-level` | `<PREFIX>LOG_LEVEL` | `INFO` |
|
|
397
|
+
| `--idle-timeout SECONDS` | `<PREFIX>IDLE_TIMEOUT` | the app's |
|
|
398
|
+
| `--trace` (hex-dump records) | `<PREFIX>TRACE` | off |
|
|
399
|
+
| `--tls-port`, `--tls-cert`, `--tls-key` | `<PREFIX>TLS_PORT`, `_TLS_CERT`, `_TLS_KEY` | off |
|
|
400
|
+
| `--no-tn3270e` | `<PREFIX>TN3270E=false` | TN3270E offered |
|
|
401
|
+
| `--codepage` (TN5250) | `<PREFIX>CODEPAGE` | `cp037` |
|
|
402
|
+
|
|
403
|
+
Other `mfmock` commands:
|
|
404
|
+
|
|
405
|
+
| Command | What it does |
|
|
406
|
+
| ------------------------------------------------------ | ------------------------------------------------------------------- |
|
|
407
|
+
| `mfmock demo [--users U:P,...]` | serves the demo host |
|
|
408
|
+
| `mfmock preview FILE.j2 [--size RxC] [--set key=json]` | prints a template's picture and fields |
|
|
409
|
+
| `mfmock capture FILE.html [--code SEQ]` | summarises a recording, or writes a template for one of its screens |
|
|
410
|
+
| `mfmock --version` | prints the version |
|
|
411
|
+
|
|
412
|
+
## Protocols
|
|
413
|
+
|
|
414
|
+
| | TN3270 / TN3270E | TN5250 |
|
|
415
|
+
| ------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
|
|
416
|
+
| Package | `mfmock.tn3270` | `mfmock.tn5250` |
|
|
417
|
+
| Negotiation | TN3270E (device type, LU name, functions) or plain TN3270; Query Reply | NEW-ENVIRON (optional), TERMINAL-TYPE, BINARY, EOR |
|
|
418
|
+
| Terminals | `IBM-3278-2` to `-5`, `IBM-3279-2` to `-5`, `-E` variants, `IBM-DYNAMIC` | `IBM-3179-2`, `IBM-5555-C01`, `IBM-5251-11`, `IBM-3477-FC` (27x132)... |
|
|
419
|
+
| Keys | Enter, PF1-24, PA1-3, Clear | Enter, F1-24, Roll Up/Down, Clear, Help, Print, Attn, SysReq |
|
|
420
|
+
| Not supported | printer sessions, DBCS | the 5250 query, Write Error Code, auto sign-on, DBCS, printers |
|
|
421
|
+
|
|
422
|
+
An app runs unchanged on both protocols. On 5250, an input field cannot start at row 1,
|
|
423
|
+
column 1. See [docs/protocols.md](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/protocols.md).
|
|
424
|
+
|
|
425
|
+
## Architecture
|
|
426
|
+
|
|
427
|
+
```
|
|
428
|
+
your app Application · @app.screen class X(Screen) · templates · catalog
|
|
429
|
+
mfmock.core.ui Conversation (navigation, keys, TRAN routing, timeouts, errors) · Layout · Frame · Theme
|
|
430
|
+
─────────────── ScreenRenderer (seam) ───────────────
|
|
431
|
+
mfmock.tn3270 Layout → 3270 orders mfmock.tn5250 Layout → 5250 WTD orders
|
|
432
|
+
─────────────── ProtocolAdapter (seam) ──────────────
|
|
433
|
+
mfmock.tn3270 TN3270(E) negotiation mfmock.tn5250 NEW-ENVIRON / TTYPE negotiation
|
|
434
|
+
mfmock.core MainframeServer · DeviceSession (TCP, TLS, LU names, logs, trace) · telnet · EBCDIC
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
These rules are enforced by `tests/test_architecture.py`:
|
|
438
|
+
|
|
439
|
+
- `mfmock.core` never imports a protocol package.
|
|
440
|
+
- The library imports only the standard library and `jinja2`.
|
|
441
|
+
- There is no global state, so several apps and protocols can run in one process.
|
|
442
|
+
|
|
443
|
+
## For AI agents and coding tools
|
|
444
|
+
|
|
445
|
+
- [`AGENTS.md`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/AGENTS.md) is for agents working on this repository: the layout, commands,
|
|
446
|
+
rules and pitfalls. [`.github/copilot-instructions.md`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/.github/copilot-instructions.md)
|
|
447
|
+
points to it.
|
|
448
|
+
- [`llms.txt`](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/llms.txt) is a short, link-indexed summary for LLMs writing code that uses
|
|
449
|
+
`mfmock`.
|
|
450
|
+
- To help agents in your own project, copy the
|
|
451
|
+
["Using mfmock from another project"](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/AGENTS.md#using-mfmock-from-another-project) snippet
|
|
452
|
+
into its agent instructions.
|
|
453
|
+
|
|
454
|
+
## Roadmap
|
|
455
|
+
|
|
456
|
+
These ideas would make `mfmock` more useful for integration and E2E testing. Contributions
|
|
457
|
+
are welcome.
|
|
458
|
+
|
|
459
|
+
1. **`mfmock serve-capture flow.html`:** serve a recording as a scripted mock, with no Python
|
|
460
|
+
(each screen follows the recorded keys).
|
|
461
|
+
2. **Declarative mocks:** screens and transitions in YAML or JSON, so teams that don't write
|
|
462
|
+
Python can define hosts.
|
|
463
|
+
3. **Fault injection:**
|
|
464
|
+
- latency and keyboard-lock delays;
|
|
465
|
+
- forced disconnects;
|
|
466
|
+
- refused or partial TN3270E negotiation, rejected terminal types;
|
|
467
|
+
- malformed or oversized records.
|
|
468
|
+
|
|
469
|
+
These test how a client handles errors.
|
|
470
|
+
|
|
471
|
+
4. **Session recording:**
|
|
472
|
+
- write every record and screen to JSON Lines;
|
|
473
|
+
- `replay` that format;
|
|
474
|
+
- a pass-through proxy that records a real host.
|
|
475
|
+
5. **Assertions that wait:**
|
|
476
|
+
- `wait_for_text(text, timeout)` and `wait_until(predicate)` for hosts that update the
|
|
477
|
+
screen on their own;
|
|
478
|
+
- `snapshot()` with golden files;
|
|
479
|
+
- richer `ReplayMismatch` diffs.
|
|
480
|
+
6. **Async API:** an `AsyncVirtualTerminal` and an async `running_server` for asyncio test
|
|
481
|
+
suites.
|
|
482
|
+
7. **Reach:**
|
|
483
|
+
- support Python 3.11+;
|
|
484
|
+
- a Docker image running `mfmock demo`;
|
|
485
|
+
- a list of clients tested against `mfmock`.
|
|
486
|
+
|
|
487
|
+
## Development
|
|
488
|
+
|
|
489
|
+
```sh
|
|
490
|
+
uv sync # Python 3.13+, installs pytest and ruff
|
|
491
|
+
uv run pytest -q # unit tests, examples and the README's samples
|
|
492
|
+
uv run pytest -q -m integration # with a real emulator (needs s3270: brew install x3270)
|
|
493
|
+
uv run ruff check .
|
|
494
|
+
npx prettier --check . # Markdown and YAML formatting (--write to fix)
|
|
495
|
+
uv build
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Every push runs the tests, Ruff and Prettier (`npx prettier --check .`). Pushes to `main`
|
|
499
|
+
publish a release to PyPI and pushes to `develop` publish a `.devN` pre-release; the version is
|
|
500
|
+
computed at build time. See [docs/publishing.md](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/docs/publishing.md).
|
|
501
|
+
|
|
502
|
+
## License
|
|
503
|
+
|
|
504
|
+
[MIT](https://github.com/bengieeee/mainframe-mocker/blob/HEAD/LICENSE)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
mfmock/__init__.py,sha256=Hf-Yk6zoO9Gn3UiM51CnmE7loIFnLHvHMS-5muxrcVE,1644
|
|
2
|
+
mfmock/__main__.py,sha256=sEZ43_q2gmGfHxgOv8TpNSrAkzvEyznw69nNach9LpM,2900
|
|
3
|
+
mfmock/cli.py,sha256=2JQfe66aBwtcWqV40jm5HC7iokDh6iwDvk1o9i362D8,3058
|
|
4
|
+
mfmock/core/__init__.py,sha256=Pp1gf0ISdIc-jPHh8Tufha5QHjUCHPDSv4hH85R_oIo,82
|
|
5
|
+
mfmock/core/capture.py,sha256=1GtQF0E80a3Syk8ZeJ9PVflW1k8hSpj0uu4ZUv2o-2s,12356
|
|
6
|
+
mfmock/core/cli.py,sha256=mi6srbbrz64wC3Y8LdEyhe8lwPMMrU-L7lzztDoaZFs,6651
|
|
7
|
+
mfmock/core/ebcdic.py,sha256=CcUWMuMj6gTBvObiiaTnB96mvt8MJs5iuWYpzlo-Q6I,604
|
|
8
|
+
mfmock/core/keys.py,sha256=1b9bpcaF_0N-eOW8XXguUCpDFMdFGRWaqTKurRDhe1M,2134
|
|
9
|
+
mfmock/core/preview.py,sha256=o-YD4AMxMoXXZN0n28U8eTt_A2sdg7XXFSUjGBcBOmo,323
|
|
10
|
+
mfmock/core/protocol.py,sha256=xucB2T58i18QmwvFWxzW9dLugYpuFAGc6SD1AXyBu28,1573
|
|
11
|
+
mfmock/core/server.py,sha256=pNAQa0yUqIjhIF01VdrSWfNfXTK2LkB9-3b-NCHdh9s,6559
|
|
12
|
+
mfmock/core/telnet.py,sha256=5FF6WbspYXtIvvPEuGRF613u7yM8rZmz9nFxv-E74vk,13153
|
|
13
|
+
mfmock/core/terminal.py,sha256=SGyMZ_f32BKoE0haiMkfFRpDpHzpUkxQc6YTRPkDjFg,1036
|
|
14
|
+
mfmock/core/testing/__init__.py,sha256=-Y63cD82dQ4PnmVtaVCpoSE-pC_TIHq-VhTnJKiGqXc,662
|
|
15
|
+
mfmock/core/testing/emulator.py,sha256=6HzDg5HNKMuAybYFxa3xjOI29tY9urUi6bjkw6peMEA,1274
|
|
16
|
+
mfmock/core/testing/replay.py,sha256=u-AdO0_4WMFg6Jc03DVFwR2glCYbYGF5Y-j1bkN_rJ4,7193
|
|
17
|
+
mfmock/core/testing/server.py,sha256=f6Y2qKqnUlEHbAjLlQ7V83EfTSFExgCwqBgs1EhZFdU,1517
|
|
18
|
+
mfmock/core/testing/transport.py,sha256=j1xNiIbwn3TdjKMHtV1orTc3GJzigOmN03CsFJi1dH0,6307
|
|
19
|
+
mfmock/core/testing/virtual_terminal.py,sha256=8jyi4_dG5L-HZd8Z3kT1-67-mlF4S0MACdyalw0OxhE,8058
|
|
20
|
+
mfmock/core/ui/__init__.py,sha256=ynXw7c6cWGK3HPmHIAHOYmmVflLS_3PDYEDZ6GpKEOA,1277
|
|
21
|
+
mfmock/core/ui/app.py,sha256=wRGERZrk78I39h9uXdRZSUjNxIg_NXCI8u6vUFt_fn0,6059
|
|
22
|
+
mfmock/core/ui/conversation.py,sha256=qBT08bZemEqBAVSV6fRgREyVaW1NUZmeBw8BJFwGTxw,7637
|
|
23
|
+
mfmock/core/ui/frame.py,sha256=FK20B5s9VHlnkERch2yDfpCP-EzHsarQ92cRXTohELw,3311
|
|
24
|
+
mfmock/core/ui/layout.py,sha256=639G-j-Zc7Qa9jnq6e56Io8lNY6ihlu8o2aYPRszFeE,8575
|
|
25
|
+
mfmock/core/ui/messages.py,sha256=P0gnjWHfGZsylZilVhYzNFQ2LR1Jw9Aj1H2nNybjCGs,5017
|
|
26
|
+
mfmock/core/ui/results.py,sha256=25zUyNrfQK-fJrMBSCCa8x6SLCMbeLMt9fg4CpC00YA,1054
|
|
27
|
+
mfmock/core/ui/screen.py,sha256=QD4vUt6dCmybDqlhEOE7rSVWkcU6MbtF1-PoSHhWQko,5451
|
|
28
|
+
mfmock/core/ui/style.py,sha256=xWQifnq6PT_y0u2hkngbxuQ25q2zgVXzJ8vW2BgQPaU,3730
|
|
29
|
+
mfmock/core/ui/template.py,sha256=RpNqvjqFS3aORRk0ZD3mmrwUVrUaqIVvoieUV2UlNhw,20324
|
|
30
|
+
mfmock/core/ui/validators.py,sha256=f8BO4562i-EChTeYeSJyxMPr5OVS9_NK7Vp5mgZ3-8M,1504
|
|
31
|
+
mfmock/core/ui/widgets.py,sha256=N8E4YfogWI3iRasZdkIzQJNW_wbbeFTq8bqyD5BIZ2I,5989
|
|
32
|
+
mfmock/demo/__init__.py,sha256=_0lUe_mlqyYZzF1of_NN_1F-NtH4GvrMhZaQwCt0i3Y,5297
|
|
33
|
+
mfmock/demo/templates/fields.j2,sha256=8C69O9ZcvojOV75wccILHFFare7F0jCVvv58O2Z78RI,1043
|
|
34
|
+
mfmock/demo/templates/hello.j2,sha256=rLqpzfCQRfNAOj3PFCSNrUBHqnqXhs5Px4ch3QCNd0I,258
|
|
35
|
+
mfmock/demo/templates/signon.j2,sha256=bixR5hVaYjhDAqArpL8KVpMt_fcGGt0lNSlS_WH-2IA,580
|
|
36
|
+
mfmock/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
+
mfmock/pytest_plugin.py,sha256=MOhX1nFKoRk8rWSWTROkrKzQkqz2fxNOV23OKL1bdeA,5738
|
|
38
|
+
mfmock/templates/goodbye.j2,sha256=ejhzv5IZoajtJ55uYjyk2QgPp2y1ymzR2rniJkYOXbQ,239
|
|
39
|
+
mfmock/templates/help.j2,sha256=H3PGoLqMuVC6gEZlUJ0BNxkLamUUUgGJU9Th7CLPlEc,259
|
|
40
|
+
mfmock/templates/list.j2,sha256=d1QraF5w8FBmNLTOMR2H0NycMbGAIHmWIK3CNO6z2Vc,137
|
|
41
|
+
mfmock/templates/menu.j2,sha256=9kPfJRve3CVkK3k79u_f4ST4UgjZou8msw6KZPQsVoc,151
|
|
42
|
+
mfmock/templates/widgets.j2,sha256=mP1ueEADF7-2mcmEihIhKmVFwebPZ9Sz6XfOrvi1N50,1399
|
|
43
|
+
mfmock/tn3270/__init__.py,sha256=a_o589pH9wSlFPOnYM-hIbrOEkrTxnoUxlU3dZQGQ8s,324
|
|
44
|
+
mfmock/tn3270/adapter.py,sha256=VLsYNdmi5_7Zs-ei88vMur75bHk6EJtSujnaUzVn1lo,2631
|
|
45
|
+
mfmock/tn3270/cli.py,sha256=g6gZZ57LueVQQ8JOioubcH-CQzJe08Oj8SSv7w0kPnY,550
|
|
46
|
+
mfmock/tn3270/datastream.py,sha256=FdUR7MgUdMnRudgBdyIMnMDkr9JgGaPvDziMtKqJNGM,9313
|
|
47
|
+
mfmock/tn3270/emulator.py,sha256=6xreOcalMEOwaJWjvz74SMshCXTSJF-bxFcSKRnoSW4,11317
|
|
48
|
+
mfmock/tn3270/negotiation.py,sha256=TsAMvHTW-BL6g5YIOY3LTe7viXeCadeYWuH1SglTx4k,5647
|
|
49
|
+
mfmock/tn3270/renderer.py,sha256=sSzKPPHqX7HR5AevueqxNarduLowCY4lYyCuSXMQa90,5551
|
|
50
|
+
mfmock/tn3270/s3270.py,sha256=28_VWlFYg4Mlj2p9AGoYWkkJATQmmeIDYp9MmnYQ9J4,3839
|
|
51
|
+
mfmock/tn3270/terminal.py,sha256=vIyHI2j2PFgmwzcfhpfxcoYL8v9K1Yr1yJOBVdzOkas,1287
|
|
52
|
+
mfmock/tn3270/testing.py,sha256=GUc8RsYHKNqVf7AOpQzKQj-um0PFk6p8x30_pTXKIwM,2930
|
|
53
|
+
mfmock/tn5250/__init__.py,sha256=4cLXnXW5a6iFkKOxEM1nzmvYLudIclccxssdODJHPME,329
|
|
54
|
+
mfmock/tn5250/adapter.py,sha256=Dt1HyVHqp3l1n4sHiq_LswJc4231WH691bGK47McCjA,1495
|
|
55
|
+
mfmock/tn5250/cli.py,sha256=hWmeS-BOtIPJazdOEEpN32UVXAJJ7yR8I0eyQvuXpnE,854
|
|
56
|
+
mfmock/tn5250/datastream.py,sha256=b-KGZbVfKr11wJwGHBGhWOavxlqrswDdaT5DFXyX0nM,9258
|
|
57
|
+
mfmock/tn5250/emulator.py,sha256=BNtybmAEM6MraIVQkszUCjI4LjyFeJ4-b29lwjVNGok,12907
|
|
58
|
+
mfmock/tn5250/negotiation.py,sha256=4OvW27k-C60IubQZUvsJgvP92aZLulBZpwJf02wAmXM,3924
|
|
59
|
+
mfmock/tn5250/renderer.py,sha256=_u_89wBjqJCBYABr2VzK8DaBrai-uKilTYZTAEtD3yA,7217
|
|
60
|
+
mfmock/tn5250/terminal.py,sha256=hSJojYX4cgDaEq9xmH_N7IBwE4ZgfnV0eviZDTrIHDs,1260
|
|
61
|
+
mfmock/tn5250/testing.py,sha256=YigxvdW0MzuFxgvDpP_GMyyPO7MrwYmrVnkIb66yRqk,2337
|
|
62
|
+
mainframe_mocker-0.1.0.dist-info/licenses/LICENSE,sha256=YugU-STp9FsT407GRyq3cMcO9RjsizqXPlKnq1j6f04,1070
|
|
63
|
+
mainframe_mocker-0.1.0.dist-info/WHEEL,sha256=1dD_GoEqEIxQ7VnrUExgdj_eExY4b4UgfR0iBlGKVac,80
|
|
64
|
+
mainframe_mocker-0.1.0.dist-info/entry_points.txt,sha256=TSq_ITdEoWbYnzmqVStY8SQXh2Wa8GjEEaaWBZLoYYw,91
|
|
65
|
+
mainframe_mocker-0.1.0.dist-info/METADATA,sha256=3lqva-CDZ0tXhFkBWF6TqwrholbEXGjnLY4aksH0Hk4,31716
|
|
66
|
+
mainframe_mocker-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben Goldsmith
|
|
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.
|
mfmock/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""mainframe-mocker: build mock IBM mainframe terminal applications.
|
|
2
|
+
|
|
3
|
+
Packages:
|
|
4
|
+
mfmock.core protocol-neutral server, screen toolkit, conversation engine, testing
|
|
5
|
+
mfmock.tn3270 the IBM 3270 (TN3270 / TN3270E) protocol backend
|
|
6
|
+
mfmock.tn5250 the IBM 5250 (TN5250) protocol backend
|
|
7
|
+
|
|
8
|
+
The screen toolkit is re-exported here, so a whole mock can be written against ``mfmock``::
|
|
9
|
+
|
|
10
|
+
from mfmock import Application, Screen, Stay, Exit, required, run
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .core.keys import InputEvent, Key
|
|
20
|
+
from .core.server import DeviceSession, MainframeServer, ServerConfig
|
|
21
|
+
from .core.terminal import Terminal
|
|
22
|
+
from .core.ui import * # noqa: F403
|
|
23
|
+
from .core.ui import __all__ as _ui_all
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
__version__ = version("mainframe-mocker")
|
|
27
|
+
except PackageNotFoundError: # running from a source tree without installing
|
|
28
|
+
__version__ = "0.0.0"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run(app: Any, argv: Sequence[str] | None = None, *, protocols: Sequence[str] | None = None,
|
|
32
|
+
**kwargs: Any) -> None:
|
|
33
|
+
"""Serve ``app`` with the standard flags (see ``mfmock.core.cli.run``).
|
|
34
|
+
|
|
35
|
+
By default over TN3270. With ``protocols=("tn3270", "tn5250")`` a ``--protocol {3270,5250}``
|
|
36
|
+
flag (or ``<env_prefix>PROTOCOL``) chooses; the first protocol is the default.
|
|
37
|
+
"""
|
|
38
|
+
from .cli import adapter_options
|
|
39
|
+
from .core.cli import run as _run
|
|
40
|
+
|
|
41
|
+
_run(app, adapter_options(protocols), argv, **kwargs)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
__all__ = [*_ui_all, "DeviceSession", "InputEvent", "Key", "MainframeServer", "ServerConfig", "Terminal", "run"]
|