green-screen-client 1.2.0__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.
@@ -0,0 +1,18 @@
1
+ node_modules/
2
+ dist/
3
+ *.tsbuildinfo
4
+ .DS_Store
5
+ .claude/
6
+ apps/demo/.env
7
+ apps/demo/.env.local
8
+ apps/demo/e2e/screenshots/
9
+ .wrangler/
10
+ test-results/
11
+ .env
12
+ reference/
13
+ debug/
14
+ DEBUG_NOTES.md
15
+ BUG_LOG.md
16
+ *.code-workspace
17
+ __pycache__/
18
+ *.pyc
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: green-screen-client
3
+ Version: 1.2.0
4
+ Summary: Python client for green-screen-proxy — typed async REST + WebSocket adapter for TN5250/TN3270/VT/HP6530 terminal emulation
5
+ Project-URL: Homepage, https://github.com/legacybridge-software/green-screen-react
6
+ Project-URL: Repository, https://github.com/legacybridge-software/green-screen-react
7
+ Project-URL: Issues, https://github.com/legacybridge-software/green-screen-react/issues
8
+ License: MIT
9
+ Keywords: as400,emulator,green-screen,ibm-i,terminal,tn3270,tn5250
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Terminals :: Terminal Emulators/X Terminals
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: websockets>=12.0
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
25
+ Requires-Dist: pytest>=8.0; extra == 'test'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # green-screen-client
29
+
30
+ Python client for [`green-screen-proxy`](https://github.com/legacybridge-software/green-screen-react/tree/main/packages/proxy) — typed async REST + WebSocket adapter for TN5250, TN3270, VT, and HP 6530 terminal emulation.
31
+
32
+ This is a **standalone package**. It is not bundled with or required by `green-screen-react` — install it separately when your integration runs on Python.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ pip install green-screen-client
38
+ ```
39
+
40
+ ## Three layers, pick what fits
41
+
42
+ ### 1. Low-level REST (`RestClient`)
43
+
44
+ Thin wrapper over the proxy's HTTP endpoints. One method per endpoint, dataclasses in/out.
45
+
46
+ ```python
47
+ from green_screen_client import RestClient, ConnectConfig
48
+
49
+ async with RestClient("http://proxy:3001") as client:
50
+ await client.connect(ConnectConfig(
51
+ host="pub400.com",
52
+ protocol="tn5250",
53
+ username="alice",
54
+ password="secret",
55
+ ))
56
+ screen = await client.get_screen()
57
+ print(screen.content)
58
+ await client.send_text("1")
59
+ await client.send_key("Enter")
60
+ ```
61
+
62
+ ### 2. Low-level WebSocket (`WsClient`)
63
+
64
+ Single WebSocket, real-time screen pushes, reattach for session recovery, lifecycle events (`session.lost`, `session.resumed`).
65
+
66
+ ```python
67
+ from green_screen_client import WsClient
68
+
69
+ async with WsClient("http://proxy:3001") as client:
70
+ await client.reattach("abc-123") # reattach after page reload / process restart
71
+ client.on_screen(lambda s: print("screen update:", s.cursor_row, s.cursor_col))
72
+ client.on_session_lost(lambda sid, status: print("lost:", sid, status.status))
73
+ async for event in client.events():
74
+ if event.type == "screen":
75
+ ...
76
+ ```
77
+
78
+ ### 3. High-level `ProxyTerminalClient` + `ScreenBuffer`
79
+
80
+ Drop-in shape for integrations that already read `client.screen.fields`, `client.screen.cursor_row`, etc.
81
+
82
+ ```python
83
+ from green_screen_client import ProxyTerminalClient
84
+
85
+ async with ProxyTerminalClient("http://proxy:3001", host="pub400.com") as client:
86
+ await client.login("alice", "secret")
87
+ await client.send_key("PF3")
88
+ print(client.screen.cursor_row, client.screen.cursor_col)
89
+ for field in client.screen.fields:
90
+ print(field)
91
+ ```
92
+
93
+ ## v1.2.0 primitives
94
+
95
+ - `read_mdt(modified_only=True)` — cheap post-write verification via per-field MDT bits.
96
+ - `resume_session(session_id)` — REST probe for "is this session still alive?"
97
+ - `mark_authenticated(username)` — flip session status after your own sign-on cascade (the proxy stays protocol-generic).
98
+ - `wait_for_fields(min_fields, timeout_ms=...)` — wait until a form with N input fields appears.
99
+
100
+ ## License
101
+
102
+ MIT.
@@ -0,0 +1,75 @@
1
+ # green-screen-client
2
+
3
+ Python client for [`green-screen-proxy`](https://github.com/legacybridge-software/green-screen-react/tree/main/packages/proxy) — typed async REST + WebSocket adapter for TN5250, TN3270, VT, and HP 6530 terminal emulation.
4
+
5
+ This is a **standalone package**. It is not bundled with or required by `green-screen-react` — install it separately when your integration runs on Python.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install green-screen-client
11
+ ```
12
+
13
+ ## Three layers, pick what fits
14
+
15
+ ### 1. Low-level REST (`RestClient`)
16
+
17
+ Thin wrapper over the proxy's HTTP endpoints. One method per endpoint, dataclasses in/out.
18
+
19
+ ```python
20
+ from green_screen_client import RestClient, ConnectConfig
21
+
22
+ async with RestClient("http://proxy:3001") as client:
23
+ await client.connect(ConnectConfig(
24
+ host="pub400.com",
25
+ protocol="tn5250",
26
+ username="alice",
27
+ password="secret",
28
+ ))
29
+ screen = await client.get_screen()
30
+ print(screen.content)
31
+ await client.send_text("1")
32
+ await client.send_key("Enter")
33
+ ```
34
+
35
+ ### 2. Low-level WebSocket (`WsClient`)
36
+
37
+ Single WebSocket, real-time screen pushes, reattach for session recovery, lifecycle events (`session.lost`, `session.resumed`).
38
+
39
+ ```python
40
+ from green_screen_client import WsClient
41
+
42
+ async with WsClient("http://proxy:3001") as client:
43
+ await client.reattach("abc-123") # reattach after page reload / process restart
44
+ client.on_screen(lambda s: print("screen update:", s.cursor_row, s.cursor_col))
45
+ client.on_session_lost(lambda sid, status: print("lost:", sid, status.status))
46
+ async for event in client.events():
47
+ if event.type == "screen":
48
+ ...
49
+ ```
50
+
51
+ ### 3. High-level `ProxyTerminalClient` + `ScreenBuffer`
52
+
53
+ Drop-in shape for integrations that already read `client.screen.fields`, `client.screen.cursor_row`, etc.
54
+
55
+ ```python
56
+ from green_screen_client import ProxyTerminalClient
57
+
58
+ async with ProxyTerminalClient("http://proxy:3001", host="pub400.com") as client:
59
+ await client.login("alice", "secret")
60
+ await client.send_key("PF3")
61
+ print(client.screen.cursor_row, client.screen.cursor_col)
62
+ for field in client.screen.fields:
63
+ print(field)
64
+ ```
65
+
66
+ ## v1.2.0 primitives
67
+
68
+ - `read_mdt(modified_only=True)` — cheap post-write verification via per-field MDT bits.
69
+ - `resume_session(session_id)` — REST probe for "is this session still alive?"
70
+ - `mark_authenticated(username)` — flip session status after your own sign-on cascade (the proxy stays protocol-generic).
71
+ - `wait_for_fields(min_fields, timeout_ms=...)` — wait until a form with N input fields appears.
72
+
73
+ ## License
74
+
75
+ MIT.
@@ -0,0 +1,67 @@
1
+ """green-screen-client — Python client for green-screen-proxy.
2
+
3
+ Typed async REST + WebSocket adapter that mirrors the wire format of
4
+ `green-screen-types` and the adapter contract of `green-screen-react`.
5
+
6
+ Three layers, pick the one that matches your integration style:
7
+
8
+ 1. `RestClient` — low-level async REST wrapper. One method per HTTP
9
+ endpoint; raw dataclasses in/out; bring your own session management.
10
+
11
+ 2. `WsClient` — low-level async WebSocket wrapper. Single WS, real-time
12
+ `onScreen`/`onStatus`/`onSessionLost` callbacks plus an async event
13
+ iterator.
14
+
15
+ 3. `ProxyTerminalClient` + `ScreenBuffer` — high-level drop-in for
16
+ integrations that already have code reading `client.screen.fields`,
17
+ `client.screen.cursor_row`, etc. Owns the REST client and maintains
18
+ an up-to-date buffer cache on every operation.
19
+
20
+ Published separately from `green-screen-react` — install via PyPI:
21
+
22
+ pip install green-screen-client
23
+ """
24
+
25
+ from .buffer import ProxyTerminalClient, ScreenBuffer
26
+ from .rest import RestClient
27
+ from .types import (
28
+ CellExtAttr,
29
+ ConnectConfig,
30
+ ConnectionStatus,
31
+ Field,
32
+ FieldColor,
33
+ FieldValue,
34
+ ProtocolType,
35
+ ScreenData,
36
+ SelectionChoice,
37
+ SelectionField,
38
+ SendResult,
39
+ ShiftType,
40
+ Window,
41
+ )
42
+ from .ws import WsClient, WsEvent
43
+
44
+ __version__ = "1.2.0"
45
+
46
+ __all__ = [
47
+ # Clients
48
+ "RestClient",
49
+ "WsClient",
50
+ "WsEvent",
51
+ "ProxyTerminalClient",
52
+ "ScreenBuffer",
53
+ # Types
54
+ "CellExtAttr",
55
+ "ConnectConfig",
56
+ "ConnectionStatus",
57
+ "Field",
58
+ "FieldColor",
59
+ "FieldValue",
60
+ "ProtocolType",
61
+ "ScreenData",
62
+ "SelectionChoice",
63
+ "SelectionField",
64
+ "SendResult",
65
+ "ShiftType",
66
+ "Window",
67
+ ]
@@ -0,0 +1,261 @@
1
+ """ScreenBuffer-style convenience wrapper over RestClient.
2
+
3
+ Provides a synchronous-looking cached view of the current screen state
4
+ (cursor position, fields, content, signature, OIA flags) that mirrors the
5
+ attribute shape many IBM i integrations already use. Each `refresh()` or
6
+ mutating operation updates the cache from the underlying RestClient.
7
+
8
+ This module exists so that existing Python integrations that were reading
9
+ attributes like `client.screen.fields`, `client.screen.cursor_row` can
10
+ adopt green-screen-client with minimal code churn.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import List, Optional
17
+
18
+ from .rest import RestClient
19
+ from .types import ConnectConfig, ConnectionStatus, FieldValue, ScreenData, SendResult
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ScreenBuffer:
25
+ """Mutable cache of the current screen state.
26
+
27
+ Fields are populated from the most recent RestClient response. A fresh
28
+ ScreenBuffer (before any operation has been performed) has empty
29
+ strings and zero counters — callers should check `.content` or call
30
+ `refresh()` before reading.
31
+ """
32
+
33
+ def __init__(self) -> None:
34
+ self.rows: int = 24
35
+ self.cols: int = 80
36
+ self.cursor_row: int = 0
37
+ self.cursor_col: int = 0
38
+ self.content: str = ""
39
+ self.screen_signature: str = ""
40
+ self.fields: List[dict] = []
41
+ self.keyboard_locked: bool = False
42
+ self.message_waiting: bool = False
43
+ self.alarm: bool = False
44
+ self.insert_mode: bool = False
45
+ self.windows: List[dict] = []
46
+ self.selection_fields: List[dict] = []
47
+ self.screen_stack_depth: int = 0
48
+ self.is_popup: bool = False
49
+ self.ext_attrs: dict = {}
50
+ self.dbcs_cont: List[int] = []
51
+ self.code_page: str = "cp37"
52
+
53
+ def apply(self, screen: Optional[ScreenData]) -> None:
54
+ if screen is None:
55
+ return
56
+ self.rows = screen.rows
57
+ self.cols = screen.cols
58
+ self.cursor_row = screen.cursor_row
59
+ self.cursor_col = screen.cursor_col
60
+ self.content = screen.content
61
+ self.screen_signature = screen.screen_signature
62
+ self.fields = [self._field_to_dict(f) for f in screen.fields]
63
+ self.keyboard_locked = bool(screen.keyboard_locked)
64
+ self.message_waiting = bool(screen.message_waiting)
65
+ self.alarm = bool(screen.alarm)
66
+ self.insert_mode = bool(screen.insert_mode)
67
+ self.windows = [vars(w) for w in (screen.windows or [])]
68
+ self.selection_fields = [
69
+ {
70
+ "row": sf.row,
71
+ "col": sf.col,
72
+ "num_rows": sf.num_rows,
73
+ "num_cols": sf.num_cols,
74
+ "choices": [vars(c) for c in sf.choices],
75
+ }
76
+ for sf in (screen.selection_fields or [])
77
+ ]
78
+ self.screen_stack_depth = screen.screen_stack_depth or 0
79
+ self.is_popup = bool(screen.is_popup)
80
+ self.ext_attrs = {k: vars(v) for k, v in (screen.ext_attrs or {}).items()}
81
+ self.dbcs_cont = list(screen.dbcs_cont or [])
82
+ self.code_page = screen.code_page or "cp37"
83
+
84
+ @staticmethod
85
+ def _field_to_dict(field) -> dict: # type: ignore[no-untyped-def]
86
+ # Reconstruct a dict shape that legacy code expects, including the
87
+ # conventional 5250 'attr' byte (bit 0x08 = protected).
88
+ attr = 0x28 if field.is_protected else 0x20
89
+ if field.is_highlighted:
90
+ attr |= 0x02
91
+ return {
92
+ "row": field.row,
93
+ "col": field.col,
94
+ "length": field.length,
95
+ "attr": attr,
96
+ "is_input": field.is_input,
97
+ "is_protected": field.is_protected,
98
+ "is_highlighted": bool(field.is_highlighted),
99
+ "is_reverse": bool(field.is_reverse),
100
+ "is_underscored": bool(field.is_underscored),
101
+ "is_non_display": bool(field.is_non_display),
102
+ "is_mandatory": False,
103
+ "color": field.color,
104
+ "shift_type": field.shift_type,
105
+ "monocase": bool(field.monocase),
106
+ "is_dbcs": bool(field.is_dbcs),
107
+ "self_check_mod10": bool(field.self_check_mod10),
108
+ "self_check_mod11": bool(field.self_check_mod11),
109
+ "resequence": field.resequence,
110
+ "progression_id": field.progression_id,
111
+ "highlight_entry_attr": field.highlight_entry_attr,
112
+ "modified": bool(field.modified) if field.modified is not None else False,
113
+ }
114
+
115
+
116
+ class ProxyTerminalClient:
117
+ """High-level drop-in replacement for legacy ProxyTN5250Client-style
118
+ classes. Owns a RestClient + a ScreenBuffer cache and exposes the
119
+ familiar async method shape (connect, login, send_text, send_key,
120
+ set_cursor, get_screen, read_mdt, disconnect).
121
+
122
+ Example (LegacyBridge migration path):
123
+
124
+ client = ProxyTerminalClient("http://proxy:3001", host="pub400.com")
125
+ await client.connect()
126
+ await client.login("alice", "secret") # uses proxy auto sign-on
127
+ await client.send_key("PF3")
128
+ print(client.screen.cursor_row, client.screen.cursor_col)
129
+ await client.disconnect()
130
+ """
131
+
132
+ def __init__(
133
+ self,
134
+ proxy_url: str,
135
+ *,
136
+ host: str,
137
+ port: int = 23,
138
+ protocol: str = "tn5250",
139
+ terminal_type: Optional[str] = None,
140
+ timeout: float = 30.0,
141
+ ) -> None:
142
+ self._rest = RestClient(proxy_url, timeout=timeout)
143
+ self._host = host
144
+ self._port = port
145
+ self._protocol = protocol
146
+ self._terminal_type = terminal_type
147
+ self.screen = ScreenBuffer()
148
+ self._connected = False
149
+ self._error_message: Optional[str] = None
150
+
151
+ @property
152
+ def is_connected(self) -> bool:
153
+ return self._connected
154
+
155
+ @property
156
+ def session_id(self) -> Optional[str]:
157
+ return self._rest.session_id
158
+
159
+ @property
160
+ def error_message(self) -> Optional[str]:
161
+ return self._error_message
162
+
163
+ async def __aenter__(self) -> "ProxyTerminalClient":
164
+ return self
165
+
166
+ async def __aexit__(self, *args: object) -> None:
167
+ await self.disconnect()
168
+
169
+ async def connect(self) -> bool:
170
+ result = await self._rest.connect(
171
+ ConnectConfig(
172
+ host=self._host,
173
+ port=self._port,
174
+ protocol=self._protocol, # type: ignore[arg-type]
175
+ terminal_type=self._terminal_type,
176
+ connect_timeout=int(self._rest._timeout * 1000),
177
+ )
178
+ )
179
+ self._connected = result.success
180
+ self._error_message = result.error
181
+ # Snap an initial screen if one is available
182
+ try:
183
+ self.screen.apply(await self._rest.get_screen())
184
+ except Exception as e:
185
+ logger.debug("initial get_screen after connect failed: %s", e)
186
+ return result.success
187
+
188
+ async def login(self, username: str, password: str) -> bool:
189
+ """Connect with the proxy's built-in auto-sign-on (sends credentials
190
+ in the /connect body). For multi-step IBM i post-sign-on cascades,
191
+ prefer composing: `connect()` → type credentials → Enter → drive
192
+ intermediate screens → `mark_authenticated(username)`."""
193
+ result = await self._rest.connect(
194
+ ConnectConfig(
195
+ host=self._host,
196
+ port=self._port,
197
+ protocol=self._protocol, # type: ignore[arg-type]
198
+ terminal_type=self._terminal_type,
199
+ username=username,
200
+ password=password,
201
+ connect_timeout=int(self._rest._timeout * 1000),
202
+ )
203
+ )
204
+ self._connected = result.success
205
+ self._error_message = result.error
206
+ try:
207
+ self.screen.apply(await self._rest.get_screen())
208
+ except Exception:
209
+ pass
210
+ return result.success
211
+
212
+ async def disconnect(self) -> None:
213
+ try:
214
+ await self._rest.disconnect()
215
+ finally:
216
+ self._connected = False
217
+ await self._rest.close()
218
+
219
+ async def get_screen(self) -> str:
220
+ screen = await self._rest.get_screen()
221
+ self.screen.apply(screen)
222
+ return self.screen.content
223
+
224
+ async def send_text(self, text: str) -> bool:
225
+ result = await self._rest.send_text(text)
226
+ if result.success:
227
+ # Fetch the updated screen so callers see the effect
228
+ self.screen.apply(await self._rest.get_screen())
229
+ return result.success
230
+
231
+ async def send_key(self, key: str) -> bool:
232
+ result = await self._rest.send_key(key)
233
+ if result.success:
234
+ self.screen.apply(await self._rest.get_screen())
235
+ return result.success
236
+
237
+ async def send_enter(self) -> bool:
238
+ return await self.send_key("Enter")
239
+
240
+ async def send_tab(self) -> bool:
241
+ return await self.send_key("Tab")
242
+
243
+ async def set_cursor(self, row: int, col: int) -> bool:
244
+ result = await self._rest.set_cursor(row, col)
245
+ if result.success:
246
+ self.screen.cursor_row = result.cursor_row or row
247
+ self.screen.cursor_col = result.cursor_col or col
248
+ return result.success
249
+
250
+ async def read_mdt(self, modified_only: bool = True) -> List[FieldValue]:
251
+ return await self._rest.read_mdt(modified_only=modified_only)
252
+
253
+ async def mark_authenticated(self, username: str) -> bool:
254
+ result = await self._rest.mark_authenticated(username)
255
+ return result.success
256
+
257
+ async def wait_for_fields(self, min_fields: int, *, timeout_ms: int = 5000) -> bool:
258
+ return await self._rest.wait_for_fields(min_fields, timeout_ms=timeout_ms) is not None
259
+
260
+ async def get_status(self) -> ConnectionStatus:
261
+ return await self._rest.get_status()