ferp 0.7.1__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.
Files changed (87) hide show
  1. ferp/__init__.py +3 -0
  2. ferp/__main__.py +4 -0
  3. ferp/__version__.py +1 -0
  4. ferp/app.py +9 -0
  5. ferp/cli.py +160 -0
  6. ferp/core/__init__.py +0 -0
  7. ferp/core/app.py +1312 -0
  8. ferp/core/bundle_installer.py +245 -0
  9. ferp/core/command_provider.py +77 -0
  10. ferp/core/dependency_manager.py +59 -0
  11. ferp/core/fs_controller.py +70 -0
  12. ferp/core/fs_watcher.py +144 -0
  13. ferp/core/messages.py +49 -0
  14. ferp/core/path_actions.py +124 -0
  15. ferp/core/paths.py +3 -0
  16. ferp/core/protocols.py +8 -0
  17. ferp/core/script_controller.py +515 -0
  18. ferp/core/script_protocol.py +35 -0
  19. ferp/core/script_runner.py +421 -0
  20. ferp/core/settings.py +16 -0
  21. ferp/core/settings_store.py +69 -0
  22. ferp/core/state.py +156 -0
  23. ferp/core/task_store.py +164 -0
  24. ferp/core/transcript_logger.py +95 -0
  25. ferp/domain/__init__.py +0 -0
  26. ferp/domain/scripts.py +29 -0
  27. ferp/fscp/host/__init__.py +11 -0
  28. ferp/fscp/host/host.py +439 -0
  29. ferp/fscp/host/managed_process.py +113 -0
  30. ferp/fscp/host/process_registry.py +124 -0
  31. ferp/fscp/protocol/__init__.py +13 -0
  32. ferp/fscp/protocol/errors.py +2 -0
  33. ferp/fscp/protocol/messages.py +55 -0
  34. ferp/fscp/protocol/schemas/__init__.py +0 -0
  35. ferp/fscp/protocol/schemas/fscp/1.0/cancel.json +16 -0
  36. ferp/fscp/protocol/schemas/fscp/1.0/definitions.json +29 -0
  37. ferp/fscp/protocol/schemas/fscp/1.0/discriminator.json +14 -0
  38. ferp/fscp/protocol/schemas/fscp/1.0/envelope.json +13 -0
  39. ferp/fscp/protocol/schemas/fscp/1.0/exit.json +20 -0
  40. ferp/fscp/protocol/schemas/fscp/1.0/init.json +36 -0
  41. ferp/fscp/protocol/schemas/fscp/1.0/input_response.json +21 -0
  42. ferp/fscp/protocol/schemas/fscp/1.0/log.json +21 -0
  43. ferp/fscp/protocol/schemas/fscp/1.0/message.json +23 -0
  44. ferp/fscp/protocol/schemas/fscp/1.0/progress.json +23 -0
  45. ferp/fscp/protocol/schemas/fscp/1.0/request_input.json +47 -0
  46. ferp/fscp/protocol/schemas/fscp/1.0/result.json +16 -0
  47. ferp/fscp/protocol/schemas/fscp/__init__.py +0 -0
  48. ferp/fscp/protocol/state.py +16 -0
  49. ferp/fscp/protocol/validator.py +123 -0
  50. ferp/fscp/scripts/__init__.py +0 -0
  51. ferp/fscp/scripts/runtime/__init__.py +4 -0
  52. ferp/fscp/scripts/runtime/__main__.py +40 -0
  53. ferp/fscp/scripts/runtime/errors.py +14 -0
  54. ferp/fscp/scripts/runtime/io.py +64 -0
  55. ferp/fscp/scripts/runtime/script.py +149 -0
  56. ferp/fscp/scripts/runtime/state.py +17 -0
  57. ferp/fscp/scripts/runtime/worker.py +13 -0
  58. ferp/fscp/scripts/sdk.py +548 -0
  59. ferp/fscp/transcript/__init__.py +3 -0
  60. ferp/fscp/transcript/events.py +14 -0
  61. ferp/resources/__init__.py +0 -0
  62. ferp/services/__init__.py +3 -0
  63. ferp/services/file_listing.py +120 -0
  64. ferp/services/monday_sync.py +155 -0
  65. ferp/services/releases.py +214 -0
  66. ferp/services/scripts.py +90 -0
  67. ferp/services/update_check.py +130 -0
  68. ferp/styles/index.tcss +638 -0
  69. ferp/themes/themes.py +238 -0
  70. ferp/widgets/__init__.py +17 -0
  71. ferp/widgets/dialogs.py +167 -0
  72. ferp/widgets/file_tree.py +991 -0
  73. ferp/widgets/forms.py +146 -0
  74. ferp/widgets/output_panel.py +244 -0
  75. ferp/widgets/panels.py +13 -0
  76. ferp/widgets/process_list.py +158 -0
  77. ferp/widgets/readme_modal.py +59 -0
  78. ferp/widgets/scripts.py +192 -0
  79. ferp/widgets/task_capture.py +74 -0
  80. ferp/widgets/task_list.py +493 -0
  81. ferp/widgets/top_bar.py +110 -0
  82. ferp-0.7.1.dist-info/METADATA +128 -0
  83. ferp-0.7.1.dist-info/RECORD +87 -0
  84. ferp-0.7.1.dist-info/WHEEL +5 -0
  85. ferp-0.7.1.dist-info/entry_points.txt +2 -0
  86. ferp-0.7.1.dist-info/licenses/LICENSE +21 -0
  87. ferp-0.7.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,55 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+ from typing import Any, Mapping
4
+
5
+
6
+ class MessageDirection(Enum):
7
+ SEND = "send"
8
+ RECV = "recv"
9
+ INTERNAL = "interal"
10
+
11
+
12
+ class MessageType(Enum):
13
+ # Host -> Script
14
+ INIT = "init"
15
+ INPUT_RESPONSE = "input_response"
16
+ CANCEL = "cancel"
17
+
18
+ # Script -> Host
19
+ LOG = "log"
20
+ PROGRESS = "progress"
21
+ REQUEST_INPUT = "request_input"
22
+ RESULT = "result"
23
+ EXIT = "exit"
24
+
25
+
26
+ PROTOCOL = "ferp/1.0"
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class Message:
31
+ type: MessageType
32
+ payload: Mapping[str, Any]
33
+
34
+ def to_dict(self) -> dict[str, Any]:
35
+ return {
36
+ "protocol": PROTOCOL,
37
+ "type": self.type.value,
38
+ "payload": dict(self.payload),
39
+ }
40
+
41
+ @staticmethod
42
+ def from_dict(data: dict[str, Any]) -> "Message":
43
+ if data.get("protocol") != PROTOCOL:
44
+ raise ValueError(f"Unsupported protocol: {data.get('protocol')}")
45
+
46
+ try:
47
+ msg_type = MessageType(data["type"])
48
+ except ValueError as exc:
49
+ raise ValueError(f"Unknown message type: {data.get('type')}") from exc
50
+
51
+ payload = data.get("payload")
52
+ if not isinstance(payload, dict):
53
+ raise ValueError("payload must be an object")
54
+
55
+ return Message(type=msg_type, payload=payload)
File without changes
@@ -0,0 +1,16 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/cancel.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "allOf": [
5
+ { "$ref": "envelope.json" },
6
+ {
7
+ "properties": {
8
+ "type": { "const": "cancel" },
9
+ "payload": {
10
+ "type": "object",
11
+ "additionalProperties": false
12
+ }
13
+ }
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/definitions.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "definitions": {
5
+ "protocol": {
6
+ "type": "string",
7
+ "const": "ferp/1.0"
8
+ },
9
+
10
+ "messageType": {
11
+ "type": "string",
12
+ "enum": [
13
+ "init",
14
+ "input_response",
15
+ "cancel",
16
+ "log",
17
+ "progress",
18
+ "request_input",
19
+ "result",
20
+ "exit"
21
+ ]
22
+ },
23
+
24
+ "logLevel": {
25
+ "type": "string",
26
+ "enum": ["info", "warn", "error", "debug"]
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/message.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "oneOf": [
5
+ { "$ref": "init.json" },
6
+ { "$ref": "input_response.json" },
7
+ { "$ref": "cancel.json" },
8
+ { "$ref": "log.json" },
9
+ { "$ref": "progress.json" },
10
+ { "$ref": "request_input.json" },
11
+ { "$ref": "result.json" },
12
+ { "$ref": "exit.json" }
13
+ ]
14
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/envelope.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+
5
+ "type": "object",
6
+ "required": ["protocol", "type", "payload"],
7
+ "properties": {
8
+ "protocol": { "$ref": "definitions.json#/definitions/protocol" },
9
+ "type": { "$ref": "definitions.json#/definitions/messageType" },
10
+ "payload": { "type": "object" }
11
+ },
12
+ "additionalProperties": false
13
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/exit.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "allOf": [
5
+ { "$ref": "envelope.json" },
6
+ {
7
+ "properties": {
8
+ "type": { "const": "exit" },
9
+ "payload": {
10
+ "type": "object",
11
+ "required": ["code"],
12
+ "properties": {
13
+ "code": { "type": "integer" }
14
+ },
15
+ "additionalProperties": false
16
+ }
17
+ }
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/init.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "allOf": [
5
+ { "$ref": "envelope.json" },
6
+ {
7
+ "properties": {
8
+ "type": { "const": "init" },
9
+ "payload": {
10
+ "type": "object",
11
+ "required": ["target"],
12
+ "properties": {
13
+ "target": {
14
+ "type": "object",
15
+ "required": ["path", "kind"],
16
+ "properties": {
17
+ "path": { "type": "string" },
18
+ "kind": { "enum": ["file", "directory"] }
19
+ },
20
+ "additionalProperties": false
21
+ },
22
+ "params": {
23
+ "type": "object",
24
+ "additionalProperties": true
25
+ },
26
+ "environment": {
27
+ "type": "object",
28
+ "additionalProperties": true
29
+ }
30
+ },
31
+ "additionalProperties": false
32
+ }
33
+ }
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/input_response.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "allOf": [
5
+ { "$ref": "envelope.json" },
6
+ {
7
+ "properties": {
8
+ "type": { "const": "input_response" },
9
+ "payload": {
10
+ "type": "object",
11
+ "required": ["id", "value"],
12
+ "properties": {
13
+ "id": { "type": "string" },
14
+ "value": { "type": "string" }
15
+ },
16
+ "additionalProperties": false
17
+ }
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/log.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "allOf": [
5
+ { "$ref": "envelope.json" },
6
+ {
7
+ "properties": {
8
+ "type": { "const": "log" },
9
+ "payload": {
10
+ "type": "object",
11
+ "required": ["level", "message"],
12
+ "properties": {
13
+ "level": { "$ref": "definitions.json#/definitions/logLevel" },
14
+ "message": { "type": "string" }
15
+ },
16
+ "additionalProperties": false
17
+ }
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/message.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "type": "object",
5
+ "required": ["protocol", "type", "payload"],
6
+ "properties": {
7
+ "protocol": { "const": "ferp/1.0" },
8
+ "type": {
9
+ "enum": [
10
+ "init",
11
+ "input_response",
12
+ "cancel",
13
+ "log",
14
+ "progress",
15
+ "request_input",
16
+ "result",
17
+ "exit"
18
+ ]
19
+ },
20
+ "payload": { "type": "object" }
21
+ },
22
+ "additionalProperties": false
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/progress.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "allOf": [
5
+ { "$ref": "envelope.json" },
6
+ {
7
+ "properties": {
8
+ "type": { "const": "progress" },
9
+ "payload": {
10
+ "type": "object",
11
+ "required": ["current"],
12
+ "properties": {
13
+ "current": { "type": "number", "minimum": 0 },
14
+ "total": { "type": "number", "minimum": 0 },
15
+ "unit": { "type": "string" },
16
+ "message": { "type": "string" }
17
+ },
18
+ "additionalProperties": false
19
+ }
20
+ }
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/request_input.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "allOf": [
5
+ { "$ref": "envelope.json" },
6
+ {
7
+ "properties": {
8
+ "type": { "const": "request_input" },
9
+ "payload": {
10
+ "type": "object",
11
+ "required": ["id", "prompt"],
12
+ "properties": {
13
+ "id": { "type": "string" },
14
+ "prompt": { "type": "string" },
15
+ "default": { "type": "string" },
16
+ "secret": { "type": "boolean" },
17
+ "mode": { "enum": ["input", "confirm"] },
18
+ "show_text_input": { "type": "boolean" },
19
+ "suggestions": {
20
+ "type": "array",
21
+ "items": { "type": "string" }
22
+ },
23
+ "fields": {
24
+ "type": "array",
25
+ "items": {
26
+ "type": "object",
27
+ "required": ["id", "type", "label"],
28
+ "properties": {
29
+ "id": { "type": "string" },
30
+ "type": { "enum": ["bool", "multi_select"] },
31
+ "label": { "type": "string" },
32
+ "default": { "type": ["boolean", "array"] },
33
+ "options": {
34
+ "type": "array",
35
+ "items": { "type": "string" }
36
+ }
37
+ },
38
+ "additionalProperties": false
39
+ }
40
+ }
41
+ },
42
+ "additionalProperties": false
43
+ }
44
+ }
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$id": "https://ferp.dev/schema/fscp/1.0/result.json",
3
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
4
+ "allOf": [
5
+ { "$ref": "envelope.json" },
6
+ {
7
+ "properties": {
8
+ "type": { "const": "result" },
9
+ "payload": {
10
+ "type": "object",
11
+ "additionalProperties": true
12
+ }
13
+ }
14
+ }
15
+ ]
16
+ }
File without changes
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum, auto
4
+
5
+
6
+ class HostState(Enum):
7
+ CREATED = auto() # before subprocess spawn
8
+ PROCESS_STARTED = auto() # pipes attached, init not sent
9
+ INIT_SENT = auto() # init sent, no stdout yet
10
+ RUNNING = auto() # normal streaming
11
+ AWAITING_INPUT = auto() # request_input outstanding
12
+ CANCELLING = auto() # cancel sent
13
+ EXIT_RECEIVED = auto() # exit seen, process may still run
14
+ TERMINATED = auto() # process ended
15
+ ERR_PROTOCOL = auto()
16
+ ERR_TRANSPORT = auto()
@@ -0,0 +1,123 @@
1
+ import json
2
+ from enum import Enum, auto
3
+ from importlib.resources import files
4
+ from typing import Dict, Set
5
+
6
+ from jsonschema import Draft202012Validator
7
+ from referencing import Registry, Resource
8
+
9
+ from ferp.fscp.protocol.messages import Message, MessageType
10
+
11
+
12
+ class ProtocolError(RuntimeError):
13
+ """Raised on FSCP protocol violations."""
14
+
15
+
16
+ class Endpoint(Enum):
17
+ HOST = auto()
18
+ SCRIPT = auto()
19
+
20
+
21
+ class ProtocolValidator:
22
+ """
23
+ Enforces FSCP message directionality and schema validity.
24
+ """
25
+
26
+ HOST_TO_SCRIPT: Set[MessageType] = {
27
+ MessageType.INIT,
28
+ MessageType.INPUT_RESPONSE,
29
+ MessageType.CANCEL,
30
+ }
31
+
32
+ SCRIPT_TO_HOST: Set[MessageType] = {
33
+ MessageType.LOG,
34
+ MessageType.PROGRESS,
35
+ MessageType.REQUEST_INPUT,
36
+ MessageType.RESULT,
37
+ MessageType.EXIT,
38
+ }
39
+
40
+ ALL_MESSAGES: Set[MessageType] = HOST_TO_SCRIPT | SCRIPT_TO_HOST
41
+
42
+ def __init__(self) -> None:
43
+ self._registry = self._load_all_schemas()
44
+ self._validators = self._load_validators()
45
+
46
+ # ----------------------------
47
+ # Schema loading
48
+ # ----------------------------
49
+
50
+ def _load_all_schemas(self) -> Registry:
51
+ base = files("ferp.fscp.protocol.schemas") / "fscp" / "1.0"
52
+ registry = Registry()
53
+
54
+ for entry in base.iterdir():
55
+ if not entry.name.endswith(".json"):
56
+ continue
57
+
58
+ schema = json.loads(entry.read_text())
59
+ registry = registry.with_resource(
60
+ schema["$id"],
61
+ Resource.from_contents(schema),
62
+ )
63
+
64
+ return registry
65
+
66
+ def _load_validators(self) -> Dict[MessageType, Draft202012Validator]:
67
+ validators: Dict[MessageType, Draft202012Validator] = {}
68
+
69
+ for name in [
70
+ "init",
71
+ "input_response",
72
+ "cancel",
73
+ "log",
74
+ "progress",
75
+ "request_input",
76
+ "result",
77
+ "exit",
78
+ ]:
79
+ schema = json.loads(
80
+ (
81
+ files("ferp.fscp.protocol.schemas")
82
+ / "fscp"
83
+ / "1.0"
84
+ / f"{name}.json"
85
+ ).read_text()
86
+ )
87
+
88
+ validators[MessageType[name.upper()]] = Draft202012Validator(
89
+ schema=schema,
90
+ registry=self._registry,
91
+ )
92
+
93
+ return validators
94
+
95
+ # ----------------------------
96
+ # Validation
97
+ # ----------------------------
98
+
99
+ def validate(self, msg: Message, *, sender: Endpoint) -> None:
100
+ # Directionality
101
+ if sender is Endpoint.HOST:
102
+ if msg.type not in self.HOST_TO_SCRIPT:
103
+ raise ProtocolError(f"Host is not allowed to send '{msg.type.value}'")
104
+
105
+ elif sender is Endpoint.SCRIPT:
106
+ if msg.type not in self.SCRIPT_TO_HOST:
107
+ raise ProtocolError(f"Script is not allowed to send '{msg.type.value}'")
108
+
109
+ else:
110
+ raise ProtocolError("Unknown sender endpoint")
111
+
112
+ # Schema validation
113
+ try:
114
+ validator = self._validators[msg.type]
115
+ except KeyError:
116
+ raise ProtocolError(f"No schema registered for '{msg.type.value}'")
117
+
118
+ instance = msg.to_dict()
119
+
120
+ errors = sorted(validator.iter_errors(instance), key=str)
121
+ if errors:
122
+ err = errors[0]
123
+ raise ProtocolError(f"Invalid {msg.type.value} message: {err.message}")
File without changes
@@ -0,0 +1,4 @@
1
+ from ferp.fscp.scripts.runtime.script import ScriptRuntime
2
+ from ferp.fscp.scripts.runtime.worker import run_runtime
3
+
4
+ __all__ = ["ScriptRuntime", "run_runtime"]
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ FSCP reference script executable.
4
+
5
+ This module is intentionally thin:
6
+ - No protocol logic
7
+ - No business logic
8
+ - No error recovery
9
+
10
+ It exists only to bootstrap ScriptRuntime and exit cleanly.
11
+ """
12
+
13
+ import sys
14
+ import traceback
15
+
16
+ from ferp.fscp.scripts.runtime.errors import ScriptError
17
+ from ferp.fscp.scripts.runtime.script import ScriptRuntime
18
+
19
+
20
+ def main() -> int:
21
+ runtime = ScriptRuntime()
22
+
23
+ try:
24
+ runtime.run()
25
+ return 0
26
+
27
+ except ScriptError as exc:
28
+ # ScriptError means we violated the FSCP contract or hit a fatal state.
29
+ # Best effort: emit to stderr only (never stdout).
30
+ print(f"[FSCP SCRIPT ERROR] {exc}", file=sys.stderr)
31
+ return 2
32
+
33
+ except Exception:
34
+ # Truly unexpected failure.
35
+ traceback.print_exc(file=sys.stderr)
36
+ return 3
37
+
38
+
39
+ if __name__ == "__main__":
40
+ sys.exit(main())
@@ -0,0 +1,14 @@
1
+ class ScriptError(Exception):
2
+ """Base class for all script runtime errors."""
3
+
4
+
5
+ class ProtocolViolation(ScriptError):
6
+ """Host sent an invalid or illegal protocol message."""
7
+
8
+
9
+ class InvalidStateTransition(ScriptError):
10
+ """Message not allowed in current script state."""
11
+
12
+
13
+ class FatalScriptError(ScriptError):
14
+ """Unhandled internal error."""
@@ -0,0 +1,64 @@
1
+ import json
2
+ import sys
3
+ from multiprocessing.connection import Connection
4
+ from typing import Any, Dict, Optional
5
+
6
+ _connection: Optional[Connection] = None
7
+
8
+
9
+ def configure_connection(conn: Connection) -> None:
10
+ """Attach a multiprocessing pipe connection for script IO."""
11
+ global _connection
12
+ _connection = conn
13
+
14
+
15
+ def read_message() -> Dict[str, Any]:
16
+ """Read a single FSCP message from the configured transport."""
17
+ if _connection is not None:
18
+ try:
19
+ payload = _connection.recv()
20
+ except EOFError as exc:
21
+ raise EOFError("Host closed pipe") from exc
22
+
23
+ if not isinstance(payload, dict):
24
+ raise ValueError("Invalid payload received from host")
25
+
26
+ return payload
27
+
28
+ line = sys.stdin.readline()
29
+ if not line:
30
+ raise EOFError("Host closed stdin")
31
+
32
+ try:
33
+ return json.loads(line)
34
+ except json.JSONDecodeError as exc:
35
+ raise ValueError(f"Invalid JSON from host: {exc}") from exc
36
+
37
+
38
+ def try_read_message() -> Dict[str, Any] | None:
39
+ """Attempt to read a single message without blocking."""
40
+ if _connection is None:
41
+ return None
42
+
43
+ if not _connection.poll(0):
44
+ return None
45
+
46
+ try:
47
+ payload = _connection.recv()
48
+ except EOFError as exc:
49
+ raise EOFError("Host closed pipe") from exc
50
+
51
+ if not isinstance(payload, dict):
52
+ raise ValueError("Invalid payload received from host")
53
+
54
+ return payload
55
+
56
+
57
+ def write_message(msg: Dict[str, Any]) -> None:
58
+ """Write a single FSCP message to the configured transport."""
59
+ if _connection is not None:
60
+ _connection.send(msg)
61
+ return
62
+
63
+ sys.stdout.write(json.dumps(msg) + "\n")
64
+ sys.stdout.flush()