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.
- green_screen_client-1.2.0/.gitignore +18 -0
- green_screen_client-1.2.0/PKG-INFO +102 -0
- green_screen_client-1.2.0/README.md +75 -0
- green_screen_client-1.2.0/green_screen_client/__init__.py +67 -0
- green_screen_client-1.2.0/green_screen_client/buffer.py +261 -0
- green_screen_client-1.2.0/green_screen_client/rest.py +226 -0
- green_screen_client-1.2.0/green_screen_client/types.py +306 -0
- green_screen_client-1.2.0/green_screen_client/ws.py +269 -0
- green_screen_client-1.2.0/pyproject.toml +45 -0
- green_screen_client-1.2.0/tests/test_types.py +153 -0
|
@@ -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()
|