provide-uterm 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. provide_uterm-0.5.1/MANIFEST.in +6 -0
  2. provide_uterm-0.5.1/PKG-INFO +74 -0
  3. provide_uterm-0.5.1/README.md +41 -0
  4. provide_uterm-0.5.1/VERSION +1 -0
  5. provide_uterm-0.5.1/pyproject.toml +178 -0
  6. provide_uterm-0.5.1/setup.cfg +4 -0
  7. provide_uterm-0.5.1/src/provide/uterm/__init__.py +172 -0
  8. provide_uterm-0.5.1/src/provide/uterm/_ansi_dialects.py +205 -0
  9. provide_uterm-0.5.1/src/provide/uterm/ansi.py +346 -0
  10. provide_uterm-0.5.1/src/provide/uterm/api_routes.py +398 -0
  11. provide_uterm-0.5.1/src/provide/uterm/auth.py +344 -0
  12. provide_uterm-0.5.1/src/provide/uterm/bridge/__init__.py +19 -0
  13. provide_uterm-0.5.1/src/provide/uterm/bridge/base.py +172 -0
  14. provide_uterm-0.5.1/src/provide/uterm/bridge/contracts.py +262 -0
  15. provide_uterm-0.5.1/src/provide/uterm/bridge/coordinator.py +138 -0
  16. provide_uterm-0.5.1/src/provide/uterm/bridge/policy.py +85 -0
  17. provide_uterm-0.5.1/src/provide/uterm/bridge/schemas.py +474 -0
  18. provide_uterm-0.5.1/src/provide/uterm/cf_ws_transport.py +246 -0
  19. provide_uterm-0.5.1/src/provide/uterm/channels.py +126 -0
  20. provide_uterm-0.5.1/src/provide/uterm/colors/__init__.py +38 -0
  21. provide_uterm-0.5.1/src/provide/uterm/colors/downgrade.py +51 -0
  22. provide_uterm-0.5.1/src/provide/uterm/colors/mode.py +49 -0
  23. provide_uterm-0.5.1/src/provide/uterm/colors/rgb.py +99 -0
  24. provide_uterm-0.5.1/src/provide/uterm/colors/sgr.py +81 -0
  25. provide_uterm-0.5.1/src/provide/uterm/control/__init__.py +5 -0
  26. provide_uterm-0.5.1/src/provide/uterm/control/channel/__init__.py +6 -0
  27. provide_uterm-0.5.1/src/provide/uterm/control/channel/browser.py +9 -0
  28. provide_uterm-0.5.1/src/provide/uterm/control/channel/protocol.py +23 -0
  29. provide_uterm-0.5.1/src/provide/uterm/control/channel/worker.py +9 -0
  30. provide_uterm-0.5.1/src/provide/uterm/control/plane/__init__.py +15 -0
  31. provide_uterm-0.5.1/src/provide/uterm/control/plane/approval/__init__.py +6 -0
  32. provide_uterm-0.5.1/src/provide/uterm/control/plane/approval/store.py +18 -0
  33. provide_uterm-0.5.1/src/provide/uterm/control/plane/approval/types.py +22 -0
  34. provide_uterm-0.5.1/src/provide/uterm/control/plane/bootstrap.py +55 -0
  35. provide_uterm-0.5.1/src/provide/uterm/control/plane/capability.py +16 -0
  36. provide_uterm-0.5.1/src/provide/uterm/control/plane/errors.py +28 -0
  37. provide_uterm-0.5.1/src/provide/uterm/control/plane/graphical_target/__init__.py +29 -0
  38. provide_uterm-0.5.1/src/provide/uterm/control/plane/graphical_target/store.py +28 -0
  39. provide_uterm-0.5.1/src/provide/uterm/control/plane/graphical_target/types.py +43 -0
  40. provide_uterm-0.5.1/src/provide/uterm/control/plane/lease/__init__.py +6 -0
  41. provide_uterm-0.5.1/src/provide/uterm/control/plane/lease/store.py +18 -0
  42. provide_uterm-0.5.1/src/provide/uterm/control/plane/lease/types.py +17 -0
  43. provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/__init__.py +20 -0
  44. provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/approval_store.py +29 -0
  45. provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/engine.py +116 -0
  46. provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/graphical_target_store.py +31 -0
  47. provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/lease_store.py +26 -0
  48. provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/session_store.py +30 -0
  49. provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/token_store.py +46 -0
  50. provide_uterm-0.5.1/src/provide/uterm/control/plane/memory/transaction.py +140 -0
  51. provide_uterm-0.5.1/src/provide/uterm/control/plane/session/__init__.py +6 -0
  52. provide_uterm-0.5.1/src/provide/uterm/control/plane/session/store.py +18 -0
  53. provide_uterm-0.5.1/src/provide/uterm/control/plane/session/types.py +24 -0
  54. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/__init__.py +20 -0
  55. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/approval_store.py +77 -0
  56. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/connection.py +50 -0
  57. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/engine.py +170 -0
  58. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/graphical_target_store.py +161 -0
  59. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/lease_store.py +72 -0
  60. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/migration.py +49 -0
  61. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/schema/__init__.py +7 -0
  62. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/schema/v0001_initial.py +64 -0
  63. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/schema/v0002_audit_head.py +22 -0
  64. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/schema/v0003_graphical_targets.py +53 -0
  65. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/session_store.py +83 -0
  66. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/token_store.py +143 -0
  67. provide_uterm-0.5.1/src/provide/uterm/control/plane/sqlite/transaction.py +39 -0
  68. provide_uterm-0.5.1/src/provide/uterm/control/plane/token/__init__.py +6 -0
  69. provide_uterm-0.5.1/src/provide/uterm/control/plane/token/store.py +24 -0
  70. provide_uterm-0.5.1/src/provide/uterm/control/plane/token/types.py +28 -0
  71. provide_uterm-0.5.1/src/provide/uterm/control/plane/transaction/__init__.py +5 -0
  72. provide_uterm-0.5.1/src/provide/uterm/control/plane/transaction/types.py +13 -0
  73. provide_uterm-0.5.1/src/provide/uterm/control/plane/types.py +32 -0
  74. provide_uterm-0.5.1/src/provide/uterm/control_channel.py +471 -0
  75. provide_uterm-0.5.1/src/provide/uterm/control_channel_builders.py +198 -0
  76. provide_uterm-0.5.1/src/provide/uterm/control_channel_patterns.py +163 -0
  77. provide_uterm-0.5.1/src/provide/uterm/deckmux/__init__.py +43 -0
  78. provide_uterm-0.5.1/src/provide/uterm/deckmux/_edge.py +36 -0
  79. provide_uterm-0.5.1/src/provide/uterm/deckmux/_hub_mixin.py +138 -0
  80. provide_uterm-0.5.1/src/provide/uterm/deckmux/_identity.py +219 -0
  81. provide_uterm-0.5.1/src/provide/uterm/deckmux/_names.py +126 -0
  82. provide_uterm-0.5.1/src/provide/uterm/deckmux/_presence.py +172 -0
  83. provide_uterm-0.5.1/src/provide/uterm/deckmux/_protocol.py +92 -0
  84. provide_uterm-0.5.1/src/provide/uterm/deckmux/_service.py +266 -0
  85. provide_uterm-0.5.1/src/provide/uterm/deckmux/_transfer.py +112 -0
  86. provide_uterm-0.5.1/src/provide/uterm/deckmux/py.typed +0 -0
  87. provide_uterm-0.5.1/src/provide/uterm/defaults.py +48 -0
  88. provide_uterm-0.5.1/src/provide/uterm/detection/__init__.py +38 -0
  89. provide_uterm-0.5.1/src/provide/uterm/detection/buffer.py +106 -0
  90. provide_uterm-0.5.1/src/provide/uterm/detection/detector.py +455 -0
  91. provide_uterm-0.5.1/src/provide/uterm/detection/detector_compile.py +140 -0
  92. provide_uterm-0.5.1/src/provide/uterm/detection/engine.py +257 -0
  93. provide_uterm-0.5.1/src/provide/uterm/detection/extractor.py +219 -0
  94. provide_uterm-0.5.1/src/provide/uterm/detection/flow.py +151 -0
  95. provide_uterm-0.5.1/src/provide/uterm/detection/input_type.py +68 -0
  96. provide_uterm-0.5.1/src/provide/uterm/detection/loader.py +31 -0
  97. provide_uterm-0.5.1/src/provide/uterm/detection/models.py +59 -0
  98. provide_uterm-0.5.1/src/provide/uterm/detection/rules.py +166 -0
  99. provide_uterm-0.5.1/src/provide/uterm/detection/saver.py +202 -0
  100. provide_uterm-0.5.1/src/provide/uterm/embed/__init__.py +55 -0
  101. provide_uterm-0.5.1/src/provide/uterm/embed/hub.py +59 -0
  102. provide_uterm-0.5.1/src/provide/uterm/embed/session.py +413 -0
  103. provide_uterm-0.5.1/src/provide/uterm/embed/telnet_upstream.py +150 -0
  104. provide_uterm-0.5.1/src/provide/uterm/embed/types.py +178 -0
  105. provide_uterm-0.5.1/src/provide/uterm/emulator.py +287 -0
  106. provide_uterm-0.5.1/src/provide/uterm/expect.py +89 -0
  107. provide_uterm-0.5.1/src/provide/uterm/file_io.py +99 -0
  108. provide_uterm-0.5.1/src/provide/uterm/filters.py +101 -0
  109. provide_uterm-0.5.1/src/provide/uterm/frames.py +71 -0
  110. provide_uterm-0.5.1/src/provide/uterm/io.py +268 -0
  111. provide_uterm-0.5.1/src/provide/uterm/line_editor.py +220 -0
  112. provide_uterm-0.5.1/src/provide/uterm/protocols.py +29 -0
  113. provide_uterm-0.5.1/src/provide/uterm/py.typed +0 -0
  114. provide_uterm-0.5.1/src/provide/uterm/recording.py +374 -0
  115. provide_uterm-0.5.1/src/provide/uterm/redaction.py +35 -0
  116. provide_uterm-0.5.1/src/provide/uterm/render/__init__.py +49 -0
  117. provide_uterm-0.5.1/src/provide/uterm/render/buffer.py +230 -0
  118. provide_uterm-0.5.1/src/provide/uterm/render/image.py +124 -0
  119. provide_uterm-0.5.1/src/provide/uterm/render/palette.py +97 -0
  120. provide_uterm-0.5.1/src/provide/uterm/render/py.typed +0 -0
  121. provide_uterm-0.5.1/src/provide/uterm/render/segments.py +142 -0
  122. provide_uterm-0.5.1/src/provide/uterm/render/sgr.py +46 -0
  123. provide_uterm-0.5.1/src/provide/uterm/replay/__init__.py +13 -0
  124. provide_uterm-0.5.1/src/provide/uterm/replay/raw.py +35 -0
  125. provide_uterm-0.5.1/src/provide/uterm/replay/viewer.py +83 -0
  126. provide_uterm-0.5.1/src/provide/uterm/sanitizer.py +57 -0
  127. provide_uterm-0.5.1/src/provide/uterm/screen.py +200 -0
  128. provide_uterm-0.5.1/src/provide/uterm/session_logger.py +313 -0
  129. provide_uterm-0.5.1/src/provide/uterm/shell/__main__.py +65 -0
  130. provide_uterm-0.5.1/src/provide/uterm/shell/_output.py +79 -0
  131. provide_uterm-0.5.1/src/provide/uterm/shell/_render.py +59 -0
  132. provide_uterm-0.5.1/src/provide/uterm/shell/_repl.py +137 -0
  133. provide_uterm-0.5.1/src/provide/uterm/shell/_sandbox.py +173 -0
  134. provide_uterm-0.5.1/src/provide/uterm/shell/commands/__init__.py +40 -0
  135. provide_uterm-0.5.1/src/provide/uterm/shell/commands/cast.py +105 -0
  136. provide_uterm-0.5.1/src/provide/uterm/shell/commands/dispatcher.py +147 -0
  137. provide_uterm-0.5.1/src/provide/uterm/shell/commands/fetch.py +64 -0
  138. provide_uterm-0.5.1/src/provide/uterm/shell/commands/help.py +74 -0
  139. provide_uterm-0.5.1/src/provide/uterm/shell/commands/kv.py +131 -0
  140. provide_uterm-0.5.1/src/provide/uterm/shell/commands/py.py +23 -0
  141. provide_uterm-0.5.1/src/provide/uterm/shell/commands/render.py +99 -0
  142. provide_uterm-0.5.1/src/provide/uterm/shell/commands/storage.py +54 -0
  143. provide_uterm-0.5.1/src/provide/uterm/shell/commands/types.py +18 -0
  144. provide_uterm-0.5.1/src/provide/uterm/shell/terminal/__init__.py +7 -0
  145. provide_uterm-0.5.1/src/provide/uterm/shell/terminal/_connector.py +223 -0
  146. provide_uterm-0.5.1/src/provide/uterm/shell/terminal/_output.py +41 -0
  147. provide_uterm-0.5.1/src/provide/uterm/telnet_session.py +148 -0
  148. provide_uterm-0.5.1/src/provide/uterm/terminal_frames.py +32 -0
  149. provide_uterm-0.5.1/src/provide/uterm/transport_session.py +554 -0
  150. provide_uterm-0.5.1/src/provide/uterm/vnc/__init__.py +23 -0
  151. provide_uterm-0.5.1/src/provide/uterm/vnc/human_relay.py +192 -0
  152. provide_uterm-0.5.1/src/provide/uterm/vnc/rfb_filter.py +149 -0
  153. provide_uterm-0.5.1/src/provide/uterm/ws_bytes.py +45 -0
  154. provide_uterm-0.5.1/src/provide/uterm/ws_session.py +144 -0
  155. provide_uterm-0.5.1/src/provide_uterm.egg-info/PKG-INFO +74 -0
  156. provide_uterm-0.5.1/src/provide_uterm.egg-info/SOURCES.txt +196 -0
  157. provide_uterm-0.5.1/src/provide_uterm.egg-info/dependency_links.txt +1 -0
  158. provide_uterm-0.5.1/src/provide_uterm.egg-info/requires.txt +17 -0
  159. provide_uterm-0.5.1/src/provide_uterm.egg-info/top_level.txt +1 -0
  160. provide_uterm-0.5.1/tests/test_ansi_render.py +272 -0
  161. provide_uterm-0.5.1/tests/test_api_routes.py +302 -0
  162. provide_uterm-0.5.1/tests/test_auth.py +227 -0
  163. provide_uterm-0.5.1/tests/test_auth_authorized_keys.py +205 -0
  164. provide_uterm-0.5.1/tests/test_auth_kill.py +42 -0
  165. provide_uterm-0.5.1/tests/test_auth_mutmut.py +408 -0
  166. provide_uterm-0.5.1/tests/test_auth_parsers.py +266 -0
  167. provide_uterm-0.5.1/tests/test_auth_resolver.py +160 -0
  168. provide_uterm-0.5.1/tests/test_cf_ws_transport.py +460 -0
  169. provide_uterm-0.5.1/tests/test_colors.py +290 -0
  170. provide_uterm-0.5.1/tests/test_control_channel_builders.py +432 -0
  171. provide_uterm-0.5.1/tests/test_control_channel_hypothesis.py +142 -0
  172. provide_uterm-0.5.1/tests/test_control_channel_ordering.py +333 -0
  173. provide_uterm-0.5.1/tests/test_control_channel_patterns.py +328 -0
  174. provide_uterm-0.5.1/tests/test_control_frame_public_api.py +116 -0
  175. provide_uterm-0.5.1/tests/test_detection_hypothesis.py +108 -0
  176. provide_uterm-0.5.1/tests/test_embed_session.py +473 -0
  177. provide_uterm-0.5.1/tests/test_embed_telnet.py +135 -0
  178. provide_uterm-0.5.1/tests/test_expect.py +122 -0
  179. provide_uterm-0.5.1/tests/test_file_io_secure.py +54 -0
  180. provide_uterm-0.5.1/tests/test_frame_builders.py +58 -0
  181. provide_uterm-0.5.1/tests/test_frontend_sri.py +117 -0
  182. provide_uterm-0.5.1/tests/test_high_roi_mutmut.py +440 -0
  183. provide_uterm-0.5.1/tests/test_io.py +405 -0
  184. provide_uterm-0.5.1/tests/test_io_mutations.py +295 -0
  185. provide_uterm-0.5.1/tests/test_io_mutations_2.py +482 -0
  186. provide_uterm-0.5.1/tests/test_io_mutations_3.py +362 -0
  187. provide_uterm-0.5.1/tests/test_io_mutmut.py +330 -0
  188. provide_uterm-0.5.1/tests/test_mutant_kills_emulator.py +324 -0
  189. provide_uterm-0.5.1/tests/test_package_lazy_attr.py +31 -0
  190. provide_uterm-0.5.1/tests/test_protocol_negotiation.py +69 -0
  191. provide_uterm-0.5.1/tests/test_reader_loop_logger_trace.py +89 -0
  192. provide_uterm-0.5.1/tests/test_recording_permissions.py +221 -0
  193. provide_uterm-0.5.1/tests/test_redaction.py +25 -0
  194. provide_uterm-0.5.1/tests/test_sanitizer.py +31 -0
  195. provide_uterm-0.5.1/tests/test_typed_channels.py +152 -0
  196. provide_uterm-0.5.1/tests/test_vnc_human_relay.py +436 -0
  197. provide_uterm-0.5.1/tests/test_vnc_human_relay_driver.py +176 -0
  198. provide_uterm-0.5.1/tests/test_vnc_rfb_filter.py +240 -0
@@ -0,0 +1,6 @@
1
+ prune .uterm-recordings
2
+ prune .uterm-recordings-proof
3
+ prune .tmp-proof-reel
4
+ prune .tmp-playwright-videos
5
+ prune .tmp-frontend-coverage
6
+ global-exclude *.jsonl
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: provide-uterm
3
+ Version: 0.5.1
4
+ Summary: Shared terminal I/O primitives for the provide ecosystem
5
+ Author: provide.io llc
6
+ License-Expression: AGPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/provide-io/provide-uterm
8
+ Project-URL: Repository, https://github.com/provide-io/provide-uterm
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: System :: Networking
16
+ Classifier: Topic :: Terminals
17
+ Classifier: Framework :: FastAPI
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: provide-telemetry>=0.7
21
+ Requires-Dist: aiosqlite>=0.22
22
+ Requires-Dist: pydantic>=2.0
23
+ Provides-Extra: emulator
24
+ Requires-Dist: pyte>=0.8; extra == "emulator"
25
+ Provides-Extra: ssh
26
+ Requires-Dist: asyncssh>=2.14; extra == "ssh"
27
+ Provides-Extra: client
28
+ Requires-Dist: httpx2>=2.10; extra == "client"
29
+ Provides-Extra: all
30
+ Requires-Dist: pyte>=0.8; extra == "all"
31
+ Requires-Dist: asyncssh>=2.14; extra == "all"
32
+ Requires-Dist: httpx2>=2.10; extra == "all"
33
+
34
+ # provide-uterm
35
+
36
+ Core shared package for the [provide-uterm](../../README.md) platform. It
37
+ contains terminal I/O primitives, screen parsing, replay helpers, detection,
38
+ rendering, and small operator-facing utilities used by the split companion
39
+ packages.
40
+
41
+ ## What's in this package
42
+
43
+ | Module | Purpose |
44
+ |--------|---------|
45
+ | `ansi.py`, `colors/`, `render/` | ANSI processing, color transforms, render helpers |
46
+ | `screen.py`, `detection/` | Screen cleanup, prompt parsing, semantic extraction |
47
+ | `telnet_session.py`, `line_editor.py` | Terminal session and local editing helpers |
48
+ | `replay/`, `session_logger.py` | Recording and replay utilities |
49
+ | Root package exports | Curated shared primitives such as `strip_ansi`, `normalize_colors`, `LineEditor`, and file loaders |
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ pip install 'provide-uterm[all]'
55
+ ```
56
+
57
+ See the [main README](../../README.md) for extras and quick start guides.
58
+
59
+ ## Split Packages
60
+
61
+ | Package | Owns |
62
+ |---------|--------|
63
+ | `provide-uterm` | Shared terminal primitives and root facade exports, plus `provide.uterm.bridge` (HijackableMixin, contracts, schemas) |
64
+ | `provide-uterm-server` | `provide.uterm.server` (incl. `provide.uterm.server.bridge`), `provide.uterm.gateway`, `provide.uterm.tunnel` |
65
+ | `provide-uterm-platform` | `provide.uterm.manager`, `provide.uterm.pty` |
66
+ | `provide-uterm-client` | Client-side session control and MCP tooling |
67
+
68
+ ## Related Packages
69
+
70
+ This package depends on several companion packages via workspace symlinks. See the [package ecosystem table](../../README.md#package-ecosystem) for the full list.
71
+
72
+ ## License
73
+
74
+ AGPL-3.0-or-later. Copyright (c) 2025-2026 provide.io llc.
@@ -0,0 +1,41 @@
1
+ # provide-uterm
2
+
3
+ Core shared package for the [provide-uterm](../../README.md) platform. It
4
+ contains terminal I/O primitives, screen parsing, replay helpers, detection,
5
+ rendering, and small operator-facing utilities used by the split companion
6
+ packages.
7
+
8
+ ## What's in this package
9
+
10
+ | Module | Purpose |
11
+ |--------|---------|
12
+ | `ansi.py`, `colors/`, `render/` | ANSI processing, color transforms, render helpers |
13
+ | `screen.py`, `detection/` | Screen cleanup, prompt parsing, semantic extraction |
14
+ | `telnet_session.py`, `line_editor.py` | Terminal session and local editing helpers |
15
+ | `replay/`, `session_logger.py` | Recording and replay utilities |
16
+ | Root package exports | Curated shared primitives such as `strip_ansi`, `normalize_colors`, `LineEditor`, and file loaders |
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install 'provide-uterm[all]'
22
+ ```
23
+
24
+ See the [main README](../../README.md) for extras and quick start guides.
25
+
26
+ ## Split Packages
27
+
28
+ | Package | Owns |
29
+ |---------|--------|
30
+ | `provide-uterm` | Shared terminal primitives and root facade exports, plus `provide.uterm.bridge` (HijackableMixin, contracts, schemas) |
31
+ | `provide-uterm-server` | `provide.uterm.server` (incl. `provide.uterm.server.bridge`), `provide.uterm.gateway`, `provide.uterm.tunnel` |
32
+ | `provide-uterm-platform` | `provide.uterm.manager`, `provide.uterm.pty` |
33
+ | `provide-uterm-client` | Client-side session control and MCP tooling |
34
+
35
+ ## Related Packages
36
+
37
+ This package depends on several companion packages via workspace symlinks. See the [package ecosystem table](../../README.md#package-ecosystem) for the full list.
38
+
39
+ ## License
40
+
41
+ AGPL-3.0-or-later. Copyright (c) 2025-2026 provide.io llc.
@@ -0,0 +1 @@
1
+ 0.5.1
@@ -0,0 +1,178 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+
6
+ [build-system]
7
+ requires = ["setuptools>=61.0", "wheel"]
8
+ build-backend = "setuptools.build_meta"
9
+
10
+ [project]
11
+ name = "provide-uterm"
12
+ dynamic = ["version"]
13
+ description = "Shared terminal I/O primitives for the provide ecosystem"
14
+ readme = "README.md"
15
+ requires-python = ">=3.11"
16
+ license = "AGPL-3.0-or-later"
17
+ authors = [
18
+ {name = "provide.io llc"},
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: System :: Networking",
28
+ "Topic :: Terminals",
29
+ "Framework :: FastAPI",
30
+ ]
31
+ # pydantic is a RUNTIME dependency, not a dev one: bridge/schemas.py defines the
32
+ # wire-format frame models, and the eager provide/uterm/__init__.py pulls it in
33
+ # on import. It was only ever declared in the dev extra and arrived transitively
34
+ # everywhere else, so the omission stayed invisible until a lock refresh stopped
35
+ # supplying it and CI's isolated `--package` env failed to import pydantic.
36
+ dependencies = ["provide-telemetry>=0.7", "aiosqlite>=0.22", "pydantic>=2.0"]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/provide-io/provide-uterm"
40
+ Repository = "https://github.com/provide-io/provide-uterm"
41
+
42
+ [project.optional-dependencies]
43
+ emulator = ["pyte>=0.8"]
44
+ ssh = ["asyncssh>=2.14"]
45
+ client = ["httpx2>=2.10"]
46
+ all = ["pyte>=0.8", "asyncssh>=2.14", "httpx2>=2.10"]
47
+
48
+ [tool.setuptools.dynamic]
49
+ version = {file = "VERSION"}
50
+
51
+ [tool.setuptools.packages.find]
52
+ where = ["src"]
53
+
54
+ [tool.setuptools.package-data]
55
+ "provide.uterm" = [
56
+ "py.typed",
57
+ ]
58
+
59
+ [dependency-groups]
60
+ dev = [
61
+ "asyncssh>=2.22.0",
62
+ "bandit>=1.9",
63
+ "codespell>=2.4.1",
64
+ "detect-secrets>=1.5.0",
65
+ "fastapi>=0.110",
66
+ "httpx2>=2.10",
67
+ "hypothesis>=6.151.9",
68
+ "memray>=1.0; sys_platform != 'win32'",
69
+ "mutmut>=3.3.1",
70
+ "mypy>=1.19",
71
+ "pip-audit>=2.7.0",
72
+ "pip-licenses>=5.5.0",
73
+ "pre-commit>=4.0",
74
+ "pydantic>=2.0",
75
+ "pyjwt>=2.9",
76
+ "pyte>=0.8.2",
77
+ "pytest>=9.0",
78
+ "pytest-asyncio>=1.3",
79
+ "pytest-cov>=7.0",
80
+ "pytest-playwright>=0.7.2",
81
+ "pytest-randomly>=3.15.0",
82
+ "pytest-timeout>=2.4.0",
83
+ "pytest-xdist>=3.8",
84
+ "reuse>=6.2.0",
85
+ "ruff>=0.15",
86
+ "ty>=0.0.17",
87
+ "uv>=0.10.7",
88
+ "uvicorn>=0.24",
89
+ "vulture>=2.14",
90
+ "websockets>=16.0",
91
+ "pyyaml>=6.0",
92
+ "xenon>=0.9.3",
93
+ "mcp>=2.0.0",
94
+ ]
95
+
96
+ [tool.pytest.ini_options]
97
+ consider_namespace_packages = true
98
+ asyncio_mode = "auto"
99
+ asyncio_default_fixture_loop_scope = "function"
100
+ testpaths = ["tests"]
101
+ filterwarnings = [
102
+ "ignore:websockets\\.legacy is deprecated; see .*:DeprecationWarning:websockets\\.legacy",
103
+ "ignore:websockets\\.server\\.WebSocketServerProtocol is deprecated:DeprecationWarning:uvicorn\\.protocols\\.websockets\\.websockets_impl",
104
+ "ignore:remove second argument of ws_handler:DeprecationWarning:websockets\\.legacy\\.server",
105
+ ]
106
+ markers = [
107
+ "playwright: browser-based UI tests (deselected by default, run with -m playwright)",
108
+ "mutant: mutation-testing and mutmut-focused tests (deselected by default)",
109
+ "memray: memory profiling and allocation stress tests (deselected by default, run with -m memray)",
110
+ "slow: slow tests >10s (deselected by default)",
111
+ "docker: Docker-based E2E tests requiring a running Docker daemon (deselected by default, run with -m docker)",
112
+ "unit: fast unit tests with no I/O or external dependencies",
113
+ ]
114
+ addopts = [
115
+ "--import-mode=importlib",
116
+ "-v",
117
+ "--strict-markers",
118
+ "--strict-config",
119
+ "--tb=short",
120
+ "-ra",
121
+ "-m",
122
+ "not playwright and not memray and not slow and not docker",
123
+ # Coverage scoped to this package's source dir. An import name can't be used
124
+ # (`provide.uterm` is a namespace shared by every package → would pull in
125
+ # server/client/cloudflare/… and report ~69%). This package-relative path
126
+ # only resolves when pytest's CWD is the package dir, so the gate MUST be run
127
+ # from here — CI and scripts/run_all_tests.py invoke it via
128
+ # `uv run --directory packages/provide-uterm pytest tests`. Waivers below.
129
+ "--cov=src/provide/uterm",
130
+ "--cov-branch",
131
+ "--cov-report=term-missing",
132
+ "--cov-fail-under=100",
133
+ ]
134
+
135
+ [tool.coverage.run]
136
+ # Measure the whole package source, not an enumerated module allowlist. This
137
+ # keeps newly-added modules in the 100% gate by default. Genuinely
138
+ # out-of-scope modules are listed explicitly in ``omit`` below so the waiver
139
+ # is visible and grep-able (CORE-cov, 2026-05-28 remediation).
140
+ source = ["src/provide/uterm"]
141
+ omit = [
142
+ "tests/*",
143
+ ]
144
+ branch = true
145
+
146
+ [tool.coverage.report]
147
+ fail_under = 100
148
+ show_missing = true
149
+ skip_covered = false
150
+ # Only the standard explicit waiver remains. The previous broad
151
+ # ``elif msg_type ==`` / ``more_body`` regex escape-hatches were removed
152
+ # (CORE-cov): real branch tests now cover those dispatch fall-throughs, and
153
+ # any genuinely-unreachable branch carries a per-site ``# pragma: no cover``
154
+ # with an inline justification so the waiver is visible and grep-able.
155
+ partial_branches = [
156
+ "pragma: no branch",
157
+ ]
158
+
159
+ [tool.mutmut]
160
+ max_children = 6
161
+ source_paths = ["src/provide/uterm/"]
162
+ also_copy = ["scripts/"]
163
+ do_not_mutate = [
164
+ "src/provide/uterm/frontend/",
165
+ "src/provide/uterm/transports/",
166
+ "src/provide/uterm/gateway.py",
167
+ "src/provide/uterm/server/connectors/",
168
+ ]
169
+ pytest_add_cli_args = ["--no-cov", "--timeout=30"]
170
+ pytest_add_cli_args_test_selection = [
171
+ "tests/",
172
+ "tests_mutation/",
173
+ "-m",
174
+ "not playwright",
175
+ "--ignore=tests/example/test_server_e2e.py",
176
+ "--ignore=tests/bridge/test_hypothesis.py",
177
+ "--ignore=tests/e2e",
178
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,172 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+
6
+ """provide-uterm: shared terminal I/O primitives for the provide ecosystem.
7
+
8
+ Server, gateway, bridge, manager, and telnet session APIs live in split
9
+ packages and should be imported from their explicit submodules.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import pkgutil
15
+ from importlib import import_module
16
+
17
+ # Allow other installed packages to contribute sub-packages under provide.uterm
18
+ # (e.g. provide-uterm-cloudflare contributes provide.uterm.cloudflare).
19
+ __path__ = pkgutil.extend_path(__path__, __name__)
20
+
21
+ from importlib.metadata import PackageNotFoundError, version
22
+
23
+ try:
24
+ __version__ = version("provide-uterm")
25
+ except PackageNotFoundError: # pragma: no cover
26
+ __version__ = "0.0.0"
27
+
28
+ from provide.uterm.terminal_frames import TerminalFrame, TerminalFrameDisconnectedError
29
+
30
+ from provide.uterm.ansi import (
31
+ BOLD,
32
+ CLEAR_SCREEN,
33
+ DEFAULT_PALETTE,
34
+ DEFAULT_RGB,
35
+ RESET,
36
+ normalize_colors,
37
+ preview_ansi,
38
+ register_color_dialect,
39
+ registered_dialects,
40
+ unregister_color_dialect,
41
+ upgrade_to_256,
42
+ upgrade_to_truecolor,
43
+ )
44
+ from provide.uterm.auth import (
45
+ AuthorizedKeysFileResolver,
46
+ NullResolver,
47
+ ResolvedIdentity,
48
+ SSHKeyResolver,
49
+ fingerprint_from_openssh_blob,
50
+ )
51
+ from provide.uterm.colors import (
52
+ apply_color_mode,
53
+ downgrade_to_16,
54
+ downgrade_to_256,
55
+ rgb_to_16_index,
56
+ rgb_to_256,
57
+ )
58
+ from provide.uterm.control_channel_builders import (
59
+ make_identity,
60
+ make_link_patterns,
61
+ make_presence_update,
62
+ make_resume,
63
+ make_resume_failed,
64
+ make_resume_ok,
65
+ make_session_token,
66
+ )
67
+ from provide.uterm.control_channel_patterns import LinkPattern, LinkPatternRegistry
68
+ from provide.uterm.file_io import load_ans, load_palette, load_txt
69
+ from provide.uterm.line_editor import LineEditor
70
+ from provide.uterm.render.segments import (
71
+ SEGMENT_COLOR_NAMES,
72
+ Segment,
73
+ ansi_to_segments,
74
+ tokens_to_segments,
75
+ )
76
+ from provide.uterm.screen import (
77
+ clean_screen_for_display,
78
+ decode_cp437,
79
+ encode_cp437,
80
+ extract_action_tags,
81
+ extract_key_value_pairs,
82
+ extract_menu_options,
83
+ extract_numbered_list,
84
+ normalize_terminal_text,
85
+ strip_ansi,
86
+ )
87
+ from provide.uterm.ws_bytes import (
88
+ channel_str_to_bytes,
89
+ ws_frame_to_channel_str,
90
+ )
91
+
92
+ __all__ = [
93
+ "__version__",
94
+ # ansi
95
+ "CLEAR_SCREEN",
96
+ "BOLD",
97
+ "RESET",
98
+ "DEFAULT_PALETTE",
99
+ "DEFAULT_RGB",
100
+ "normalize_colors",
101
+ "SEGMENT_COLOR_NAMES",
102
+ "Segment",
103
+ "ansi_to_segments",
104
+ "tokens_to_segments",
105
+ "preview_ansi",
106
+ "register_color_dialect",
107
+ "unregister_color_dialect",
108
+ "registered_dialects",
109
+ "upgrade_to_256",
110
+ "upgrade_to_truecolor",
111
+ # colors — downgrade counterparts
112
+ "apply_color_mode",
113
+ "downgrade_to_16",
114
+ "downgrade_to_256",
115
+ "rgb_to_16_index",
116
+ "rgb_to_256",
117
+ # file_io
118
+ "load_ans",
119
+ "load_txt",
120
+ "load_palette",
121
+ # line_editor
122
+ "LineEditor",
123
+ # screen
124
+ "strip_ansi",
125
+ "normalize_terminal_text",
126
+ "decode_cp437",
127
+ "encode_cp437",
128
+ "extract_action_tags",
129
+ "clean_screen_for_display",
130
+ "extract_menu_options",
131
+ "extract_numbered_list",
132
+ "extract_key_value_pairs",
133
+ # ws_bytes — lossless byte ↔ str shim for the inline control channel
134
+ "channel_str_to_bytes",
135
+ "ws_frame_to_channel_str",
136
+ # control_channel_builders — typed builders for ControlChannel protocol messages
137
+ "make_identity",
138
+ "make_session_token",
139
+ "make_resume",
140
+ "make_resume_ok",
141
+ "make_resume_failed",
142
+ "make_link_patterns",
143
+ "make_presence_update",
144
+ # control_channel_patterns — server-side link_patterns registry
145
+ "LinkPattern",
146
+ "LinkPatternRegistry",
147
+ # auth — pluggable SSH pubkey → identity
148
+ "AuthorizedKeysFileResolver",
149
+ "NullResolver",
150
+ "ResolvedIdentity",
151
+ "SSHKeyResolver",
152
+ # terminal frame lifecycle
153
+ "TerminalFrame",
154
+ "TerminalFrameDisconnectedError",
155
+ "fingerprint_from_openssh_blob",
156
+ # control-plane namespaces
157
+ "control_channel_namespace",
158
+ "control_plane_namespace",
159
+ "frames",
160
+ "channels",
161
+ ]
162
+
163
+ from provide.uterm.control import channel as control_channel_namespace
164
+ from provide.uterm.control import plane as control_plane_namespace
165
+
166
+
167
+ def __getattr__(name: str) -> object:
168
+ if name == "frames":
169
+ return import_module("provide.uterm.frames")
170
+ if name == "channels":
171
+ return import_module("provide.uterm.channels")
172
+ raise AttributeError(name)
@@ -0,0 +1,205 @@
1
+ #
2
+ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 provide.io llc. All rights reserved.
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ """Built-in BBS color-token dialect handlers for :mod:`provide.uterm.ansi`.
6
+
7
+ Self-contained ``str -> str`` converters for the extended-token, tilde-code,
8
+ TWGS brace-token, and pipe-code dialects, plus their lookup tables. Split out
9
+ of ``ansi.py`` to keep each module within the per-file line budget; the public
10
+ surface (``normalize_colors`` + the dialect registry) stays in ``ansi.py``,
11
+ which registers these handlers at import time.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+
18
+ _EXT_TOKEN_RE = re.compile(r"\{([FBPT])(\d{1,3})\}")
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Preview helpers (private)
22
+ # ---------------------------------------------------------------------------
23
+
24
+ _PREVIEW_COLOR_MAP = {
25
+ "k": 30,
26
+ "r": 31,
27
+ "g": 32,
28
+ "y": 33,
29
+ "b": 34,
30
+ "m": 35,
31
+ "c": 36,
32
+ "w": 37,
33
+ }
34
+
35
+ _TILDE_MAP: dict[str, tuple[str, str]] = {
36
+ "1": ("+", "g"),
37
+ "2": ("+", "w"),
38
+ "3": ("+", "c"),
39
+ "4": ("+", "r"),
40
+ "5": ("+", "m"),
41
+ "6": ("+", "y"),
42
+ "7": ("-", "w"),
43
+ "0": ("-", "x"),
44
+ "r": ("+", "r"),
45
+ "R": ("+", "r"),
46
+ "g": ("+", "g"),
47
+ "G": ("+", "g"),
48
+ "y": ("+", "y"),
49
+ "Y": ("+", "y"),
50
+ "b": ("+", "b"),
51
+ "B": ("+", "b"),
52
+ "m": ("+", "m"),
53
+ "M": ("+", "m"),
54
+ "c": ("+", "c"),
55
+ "C": ("+", "c"),
56
+ "w": ("+", "w"),
57
+ "W": ("+", "w"),
58
+ "d": ("-", "w"),
59
+ "D": ("-", "w"),
60
+ "E": ("+", "r"),
61
+ }
62
+
63
+ _BRACE_TOKEN_MAP: dict[str, str] = {
64
+ "{+c}": "\x1b[1;36m",
65
+ "{-c}": "\x1b[0;36m",
66
+ "{+r}": "\x1b[1;31m",
67
+ "{-r}": "\x1b[0;31m",
68
+ "{+g}": "\x1b[1;32m",
69
+ "{-g}": "\x1b[0;32m",
70
+ "{+y}": "\x1b[1;33m",
71
+ "{-y}": "\x1b[0;33m",
72
+ "{+b}": "\x1b[1;34m",
73
+ "{-b}": "\x1b[0;34m",
74
+ "{+m}": "\x1b[1;35m",
75
+ "{-m}": "\x1b[0;35m",
76
+ "{+w}": "\x1b[1;37m",
77
+ "{+Bw}": "\x1b[1;37m",
78
+ "{-w}": "\x1b[0;37m",
79
+ "{+k}": "\x1b[1;30m",
80
+ "{-k}": "\x1b[0;30m",
81
+ "{-x}": "\x1b[0m",
82
+ "{NK}": "\x1b[0m",
83
+ "{T}": "\x1b[1m",
84
+ "{t}": "\x1b[0m",
85
+ }
86
+
87
+
88
+ def _emit_color(polarity: str, color_char: str) -> str:
89
+ if color_char == "x":
90
+ return "\x1b[0m"
91
+ code = _PREVIEW_COLOR_MAP.get(color_char)
92
+ if code is None:
93
+ return ""
94
+ if polarity == "+":
95
+ return f"\x1b[0;1;{code}m"
96
+ return f"\x1b[0;{code}m"
97
+
98
+
99
+ # Pre-build extended token lookups to eliminate per-match f-string creation.
100
+ _EXT_F_LOOKUP = tuple(f"\x1b[38;5;{i}m" for i in range(256))
101
+ _EXT_B_LOOKUP = tuple(f"\x1b[48;5;{i}m" for i in range(256))
102
+ _EXT_P_LOOKUP = tuple(f"\x1b[{(90 + (i % 8)) if (i % 16) >= 8 else (30 + (i % 8))}m" for i in range(16))
103
+ _EXT_T_LOOKUP = tuple(f"\x1b[{(100 + (i % 8)) if (i % 16) >= 8 else (40 + (i % 8))}m" for i in range(16))
104
+
105
+
106
+ def _handle_extended_tokens(text: str) -> str:
107
+ """Convert {F###}/{B###}/{P#}/{T#} extended color tokens to ANSI escapes."""
108
+ parts: list[str] = []
109
+ last_end = 0
110
+ for m in _EXT_TOKEN_RE.finditer(text):
111
+ parts.append(text[last_end : m.start()])
112
+ kind = m.group(1)
113
+ val = int(m.group(2))
114
+ if kind == "F":
115
+ parts.append(_EXT_F_LOOKUP[val] if val < 256 else f"\x1b[38;5;{val}m")
116
+ elif kind == "B":
117
+ parts.append(_EXT_B_LOOKUP[val] if val < 256 else f"\x1b[48;5;{val}m")
118
+ elif kind == "P":
119
+ parts.append(_EXT_P_LOOKUP[val % 16])
120
+ elif kind == "T": # pragma: no cover — truecolor reserved for future use
121
+ parts.append(_EXT_T_LOOKUP[val % 16])
122
+ last_end = m.end()
123
+ if last_end == 0:
124
+ return text # no matches
125
+ parts.append(text[last_end:])
126
+ return "".join(parts)
127
+
128
+
129
+ _TILDE_RE = re.compile(r"~(.)")
130
+
131
+
132
+ # Pre-build tilde code → ANSI escape lookup to avoid per-match _emit_color calls.
133
+ _TILDE_LOOKUP: dict[str, str] = {}
134
+ for _tc, (_pol, _cc) in _TILDE_MAP.items():
135
+ _seq = _emit_color(_pol, _cc)
136
+ if _seq: # pragma: no cover — _seq always truthy for all defined tilde codes
137
+ _TILDE_LOOKUP[_tc] = _seq
138
+
139
+
140
+ def _handle_tilde_codes(text: str) -> str:
141
+ parts = _TILDE_RE.split(text)
142
+ if len(parts) == 1:
143
+ return text
144
+ for i in range(1, len(parts), 2):
145
+ parts[i] = _TILDE_LOOKUP.get(parts[i], f"~{parts[i]}")
146
+ return "".join(parts)
147
+
148
+
149
+ _BRACE_3_RE = re.compile(r"(\{[+\-][a-zA-Z]\}|\{NK\}|\{T\}|\{t\})")
150
+ _BRACE_4_RE = re.compile(r"(\{[+\-]Bw\})")
151
+
152
+
153
+ def _handle_brace_tokens(text: str) -> str:
154
+ """Convert ``{+c}``/``{-x}`` brace tokens to ANSI escapes.
155
+
156
+ Includes the TWGS-specific ``{+Bw}`` header token in addition to the
157
+ single-character color tags.
158
+ """
159
+ # Handle 5-char tokens first (longest match first to avoid conflicts)
160
+ parts4 = _BRACE_4_RE.split(text)
161
+ if len(parts4) > 1:
162
+ for i in range(1, len(parts4), 2):
163
+ parts4[i] = _BRACE_TOKEN_MAP.get(parts4[i], parts4[i])
164
+ text = "".join(parts4)
165
+ # Then 3-char and other tokens
166
+ parts3 = _BRACE_3_RE.split(text)
167
+ if len(parts3) > 1:
168
+ for i in range(1, len(parts3), 2):
169
+ parts3[i] = _BRACE_TOKEN_MAP.get(parts3[i], parts3[i])
170
+ return "".join(parts3)
171
+ return text
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Pipe codes (|00-|23) — most common BBS color format
176
+ # ---------------------------------------------------------------------------
177
+
178
+ _PIPE_RE = re.compile(r"\|(\d{2})")
179
+
180
+ # DOS color order → ANSI SGR codes
181
+ _DOS_TO_ANSI_FG = [30, 34, 32, 36, 31, 35, 33, 37] # |00-|07 (dim)
182
+ _DOS_TO_ANSI_BG = [40, 44, 42, 46, 41, 45, 43, 47] # |16-|23
183
+
184
+
185
+ _PIPE_LOOKUP: dict[str, str] = {}
186
+ for _i in range(24):
187
+ _key = f"{_i:02d}"
188
+ if _i <= 7:
189
+ _PIPE_LOOKUP[_key] = f"\x1b[{_DOS_TO_ANSI_FG[_i]}m"
190
+ elif _i <= 15:
191
+ _PIPE_LOOKUP[_key] = f"\x1b[{_DOS_TO_ANSI_FG[_i - 8] + 60}m"
192
+ else:
193
+ _PIPE_LOOKUP[_key] = f"\x1b[{_DOS_TO_ANSI_BG[_i - 16]}m"
194
+
195
+
196
+ def _handle_pipe_codes(text: str) -> str:
197
+ # split() with a capturing group keeps the matched group in the result list,
198
+ # alternating: [text, match, text, match, ...]. We replace matches in-place
199
+ # via the lookup, avoiding re.sub's per-match string rebuild.
200
+ parts = _PIPE_RE.split(text)
201
+ if len(parts) == 1:
202
+ return text # no matches — return original without allocation
203
+ for i in range(1, len(parts), 2):
204
+ parts[i] = _PIPE_LOOKUP.get(parts[i], f"|{parts[i]}")
205
+ return "".join(parts)