codeaway 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codeaway/config.py ADDED
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .agents import SurfaceMap
10
+ from .desktop import FractionalRegion
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class WindowHint:
15
+ process_path: str
16
+ title_hint: str
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class AppConfig:
21
+ bind_ip: str = "127.0.0.1"
22
+ port: int = 8765
23
+ selected_agent: str | None = None
24
+ selected_window: WindowHint | None = None
25
+ surfaces: SurfaceMap | None = None
26
+
27
+ @property
28
+ def setup_complete(self) -> bool:
29
+ return (
30
+ self.selected_agent is not None
31
+ and self.selected_window is not None
32
+ and self.surfaces is not None
33
+ )
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class ConfigLoad:
38
+ config: AppConfig
39
+ warnings: tuple[str, ...]
40
+
41
+
42
+ def default_config_path() -> Path:
43
+ if os.name == "nt":
44
+ return Path(os.environ["LOCALAPPDATA"]) / "CodeAway" / "config.json"
45
+ return Path.home() / ".config" / "codeaway" / "config.json"
46
+
47
+
48
+ def _surface_values(surface: FractionalRegion) -> list[float]:
49
+ return [surface.x, surface.y, surface.width, surface.height]
50
+
51
+
52
+ def _config_values(config: AppConfig) -> dict[str, Any]:
53
+ selected_window = None
54
+ if config.selected_window is not None:
55
+ selected_window = {
56
+ "process_path": config.selected_window.process_path,
57
+ "title_hint": config.selected_window.title_hint,
58
+ }
59
+
60
+ surfaces = None
61
+ if config.surfaces is not None:
62
+ surfaces = {
63
+ "sidebar": _surface_values(config.surfaces.sidebar),
64
+ "conversation": _surface_values(config.surfaces.conversation),
65
+ "composer": _surface_values(config.surfaces.composer),
66
+ }
67
+
68
+ return {
69
+ "bind_ip": config.bind_ip,
70
+ "port": config.port,
71
+ "selected_agent": config.selected_agent,
72
+ "selected_window": selected_window,
73
+ "surfaces": surfaces,
74
+ }
75
+
76
+
77
+ def save_config(path: str | os.PathLike[str], config: AppConfig) -> None:
78
+ destination = Path(path)
79
+ destination.parent.mkdir(parents=True, exist_ok=True)
80
+ temporary = destination.with_name(f"{destination.name}.tmp")
81
+
82
+ with temporary.open("w", encoding="utf-8") as handle:
83
+ json.dump(_config_values(config), handle, indent=2)
84
+ handle.write("\n")
85
+ handle.flush()
86
+ os.fsync(handle.fileno())
87
+ os.replace(temporary, destination)
88
+
89
+
90
+ def _number(value: Any, field: str) -> int | float:
91
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
92
+ raise ValueError(f"{field} must be a number")
93
+ return value
94
+
95
+
96
+ def _surface(value: Any) -> FractionalRegion:
97
+ if not isinstance(value, list) or len(value) != 4:
98
+ raise ValueError("surface must be an array of four numbers")
99
+ return FractionalRegion(
100
+ _number(value[0], "x"),
101
+ _number(value[1], "y"),
102
+ _number(value[2], "width"),
103
+ _number(value[3], "height"),
104
+ )
105
+
106
+
107
+ def _parse_config(value: Any) -> AppConfig:
108
+ if not isinstance(value, dict):
109
+ raise ValueError("configuration must be an object")
110
+
111
+ bind_ip = value.get("bind_ip", AppConfig.bind_ip)
112
+ if not isinstance(bind_ip, str):
113
+ raise ValueError("bind_ip must be a string")
114
+
115
+ port = value.get("port", AppConfig.port)
116
+ if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535:
117
+ raise ValueError("port must be an integer between 1 and 65535")
118
+
119
+ selected_agent = value.get("selected_agent")
120
+ if selected_agent is not None and not isinstance(selected_agent, str):
121
+ raise ValueError("selected_agent must be a string or null")
122
+
123
+ selected_window_value = value.get("selected_window")
124
+ selected_window = None
125
+ if selected_window_value is not None:
126
+ if not isinstance(selected_window_value, dict):
127
+ raise ValueError("selected_window must be an object or null")
128
+ process_path = selected_window_value.get("process_path")
129
+ title_hint = selected_window_value.get("title_hint")
130
+ if not isinstance(process_path, str) or not isinstance(title_hint, str):
131
+ raise ValueError("window hints must be strings")
132
+ selected_window = WindowHint(process_path, title_hint)
133
+
134
+ surfaces_value = value.get("surfaces")
135
+ surfaces = None
136
+ if surfaces_value is not None:
137
+ if not isinstance(surfaces_value, dict):
138
+ raise ValueError("surfaces must be an object or null")
139
+ surfaces = SurfaceMap(
140
+ sidebar=_surface(surfaces_value["sidebar"]),
141
+ conversation=_surface(surfaces_value["conversation"]),
142
+ composer=_surface(surfaces_value["composer"]),
143
+ )
144
+
145
+ return AppConfig(bind_ip, port, selected_agent, selected_window, surfaces)
146
+
147
+
148
+ def load_config(path: str | os.PathLike[str]) -> ConfigLoad:
149
+ source = Path(path)
150
+ if not source.exists():
151
+ return ConfigLoad(AppConfig(), ())
152
+ try:
153
+ with source.open("r", encoding="utf-8") as handle:
154
+ config = _parse_config(json.load(handle))
155
+ except (OSError, ValueError, TypeError, KeyError, json.JSONDecodeError):
156
+ return ConfigLoad(AppConfig(), ("Invalid configuration; using defaults.",))
157
+ return ConfigLoad(config, ())