quilt-hp-python 0.1.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 (53) hide show
  1. quilt_hp/__init__.py +22 -0
  2. quilt_hp/_paths.py +26 -0
  3. quilt_hp/_proto/__init__.py +0 -0
  4. quilt_hp/_proto/quilt_device_pairing_pb2.py +56 -0
  5. quilt_hp/_proto/quilt_device_pairing_pb2.pyi +317 -0
  6. quilt_hp/_proto/quilt_device_pairing_pb2_grpc.py +24 -0
  7. quilt_hp/_proto/quilt_hds_pb2.py +292 -0
  8. quilt_hp/_proto/quilt_hds_pb2.pyi +3947 -0
  9. quilt_hp/_proto/quilt_hds_pb2_grpc.py +1732 -0
  10. quilt_hp/_proto/quilt_notifier_pb2.py +55 -0
  11. quilt_hp/_proto/quilt_notifier_pb2.pyi +258 -0
  12. quilt_hp/_proto/quilt_notifier_pb2_grpc.py +97 -0
  13. quilt_hp/_proto/quilt_services_pb2.py +171 -0
  14. quilt_hp/_proto/quilt_services_pb2.pyi +1320 -0
  15. quilt_hp/_proto/quilt_services_pb2_grpc.py +1188 -0
  16. quilt_hp/_proto/quilt_system_pb2.py +53 -0
  17. quilt_hp/_proto/quilt_system_pb2.pyi +164 -0
  18. quilt_hp/_proto/quilt_system_pb2_grpc.py +270 -0
  19. quilt_hp/auth.py +244 -0
  20. quilt_hp/cli/__init__.py +1 -0
  21. quilt_hp/cli/main.py +770 -0
  22. quilt_hp/cli/settings.py +123 -0
  23. quilt_hp/cli/store.py +105 -0
  24. quilt_hp/cli/tui.py +2677 -0
  25. quilt_hp/client.py +616 -0
  26. quilt_hp/const.py +57 -0
  27. quilt_hp/exceptions.py +23 -0
  28. quilt_hp/models/__init__.py +85 -0
  29. quilt_hp/models/comfort.py +47 -0
  30. quilt_hp/models/controller.py +135 -0
  31. quilt_hp/models/energy.py +31 -0
  32. quilt_hp/models/enums.py +298 -0
  33. quilt_hp/models/indoor_unit.py +412 -0
  34. quilt_hp/models/outdoor_unit.py +71 -0
  35. quilt_hp/models/qsm.py +105 -0
  36. quilt_hp/models/schedule.py +98 -0
  37. quilt_hp/models/sensor.py +92 -0
  38. quilt_hp/models/software_update.py +74 -0
  39. quilt_hp/models/space.py +177 -0
  40. quilt_hp/models/system.py +451 -0
  41. quilt_hp/py.typed +1 -0
  42. quilt_hp/services/__init__.py +1 -0
  43. quilt_hp/services/hds.py +480 -0
  44. quilt_hp/services/streaming.py +561 -0
  45. quilt_hp/services/system.py +95 -0
  46. quilt_hp/services/user.py +143 -0
  47. quilt_hp/tokens.py +119 -0
  48. quilt_hp/transport.py +192 -0
  49. quilt_hp_python-0.1.1.dist-info/METADATA +172 -0
  50. quilt_hp_python-0.1.1.dist-info/RECORD +53 -0
  51. quilt_hp_python-0.1.1.dist-info/WHEEL +4 -0
  52. quilt_hp_python-0.1.1.dist-info/entry_points.txt +2 -0
  53. quilt_hp_python-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,123 @@
1
+ """Settings persistence for non-secret CLI/TUI preferences."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from dataclasses import asdict, dataclass
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path
10
+ from uuid import uuid4
11
+
12
+ from quilt_hp._paths import app_config_dir
13
+
14
+ _SETTINGS_SCHEMA_VERSION = 1
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class Settings:
19
+ """Non-secret user preferences shared by CLI and TUI."""
20
+
21
+ email: str | None = None
22
+ home: str | None = None
23
+ use_fahrenheit: bool = False
24
+ dark: bool | None = None
25
+
26
+
27
+ class SettingsStore:
28
+ """Platform-aware settings storage with migration and recovery."""
29
+
30
+ def __init__(self, path: Path | None = None) -> None:
31
+ self._path = path
32
+
33
+ def _settings_path(self) -> Path:
34
+ if self._path is not None:
35
+ return self._path
36
+ return app_config_dir() / "settings.json"
37
+
38
+ def load(self) -> Settings:
39
+ """Load settings, migrating legacy files and recovering corruption."""
40
+ path = self._settings_path()
41
+ try:
42
+ payload = json.loads(path.read_text())
43
+ except FileNotFoundError:
44
+ return Settings()
45
+ except Exception:
46
+ return self._recover_corruption("invalid-json")
47
+
48
+ if not isinstance(payload, dict):
49
+ return self._recover_corruption("invalid-shape")
50
+
51
+ schema_version = payload.get("schema_version")
52
+ if schema_version is None:
53
+ settings = self._from_legacy(payload)
54
+ self.save(settings)
55
+ return settings
56
+
57
+ if schema_version != _SETTINGS_SCHEMA_VERSION:
58
+ return self._recover_corruption("unsupported-schema")
59
+
60
+ prefs = payload.get("preferences")
61
+ if not isinstance(prefs, dict):
62
+ return self._recover_corruption("invalid-preferences")
63
+ return self._coerce(prefs)
64
+
65
+ def save(self, settings: Settings) -> None:
66
+ """Persist settings using versioned schema and atomic replace."""
67
+ payload = {
68
+ "schema_version": _SETTINGS_SCHEMA_VERSION,
69
+ "preferences": asdict(settings),
70
+ }
71
+ self._atomic_write(payload)
72
+
73
+ def update(
74
+ self,
75
+ *,
76
+ email: str | None = None,
77
+ home: str | None = None,
78
+ use_fahrenheit: bool | None = None,
79
+ dark: bool | None = None,
80
+ ) -> Settings:
81
+ """Update selected settings fields and persist."""
82
+ settings = self.load()
83
+ if email is not None:
84
+ settings.email = email
85
+ if home is not None:
86
+ settings.home = home
87
+ if use_fahrenheit is not None:
88
+ settings.use_fahrenheit = use_fahrenheit
89
+ if dark is not None:
90
+ settings.dark = dark
91
+ self.save(settings)
92
+ return settings
93
+
94
+ def _from_legacy(self, payload: dict[str, object]) -> Settings:
95
+ return self._coerce(payload)
96
+
97
+ def _coerce(self, payload: dict[str, object]) -> Settings:
98
+ email = payload.get("email")
99
+ home = payload.get("home")
100
+ dark = payload.get("dark")
101
+ return Settings(
102
+ email=email if isinstance(email, str) else None,
103
+ home=home if isinstance(home, str) else None,
104
+ use_fahrenheit=bool(payload.get("use_fahrenheit", False)),
105
+ dark=dark if isinstance(dark, bool) else None,
106
+ )
107
+
108
+ def _atomic_write(self, payload: dict[str, object]) -> None:
109
+ path = self._settings_path()
110
+ path.parent.mkdir(parents=True, exist_ok=True)
111
+ tmp = path.with_name(f"{path.name}.{os.getpid()}.{uuid4().hex}.tmp")
112
+ tmp.write_text(json.dumps(payload, indent=2))
113
+ os.replace(tmp, path)
114
+
115
+ def _recover_corruption(self, reason: str) -> Settings:
116
+ path = self._settings_path()
117
+ settings = Settings()
118
+ if path.exists():
119
+ timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
120
+ backup = path.with_name(f"{path.name}.corrupt-{timestamp}-{reason}")
121
+ path.replace(backup)
122
+ self.save(settings)
123
+ return settings
quilt_hp/cli/store.py ADDED
@@ -0,0 +1,105 @@
1
+ """Token persistence store for CLI/TUI authentication state.
2
+
3
+ Token persistence remains intentionally separate from general user
4
+ preferences. ``FileStore`` implements the core ``TokenStore`` protocol
5
+ and can be passed directly to ``QuiltClient(token_store=store)``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import os
13
+ from dataclasses import asdict
14
+ from pathlib import Path
15
+
16
+ from quilt_hp._paths import app_config_dir
17
+ from quilt_hp.exceptions import QuiltAuthError
18
+ from quilt_hp.tokens import CachedTokens
19
+
20
+
21
+ class FileStore:
22
+ """Filesystem-backed token persistence."""
23
+
24
+ # ------------------------------------------------------------------ tokens
25
+
26
+ def _token_path(self) -> Path:
27
+ return app_config_dir() / "tokens.json"
28
+
29
+ async def load(self, email: str) -> CachedTokens | None:
30
+ """TokenStore.load — return cached tokens for *email* or None."""
31
+ return await asyncio.to_thread(self._load_sync, email)
32
+
33
+ def _load_sync(self, email: str) -> CachedTokens | None:
34
+ try:
35
+ data = json.loads(self._token_path().read_text())
36
+ except FileNotFoundError:
37
+ return None
38
+ except json.JSONDecodeError as exc:
39
+ raise QuiltAuthError("Token store contains invalid JSON.") from exc
40
+ except OSError as exc:
41
+ raise QuiltAuthError("Failed to read token store.") from exc
42
+
43
+ try:
44
+ entry = data[email]
45
+ return CachedTokens(
46
+ id_token=entry["id_token"],
47
+ refresh_token=entry["refresh_token"],
48
+ expires_at=entry["expires_at"],
49
+ )
50
+ except KeyError:
51
+ return None
52
+ except (TypeError, ValueError) as exc:
53
+ raise QuiltAuthError(f"Malformed token entry for {email!r}.") from exc
54
+
55
+ async def save(self, email: str, tokens: CachedTokens) -> None:
56
+ """TokenStore.save — persist tokens for *email*."""
57
+ await asyncio.to_thread(self._save_sync, email, tokens)
58
+
59
+ def _save_sync(self, email: str, tokens: CachedTokens) -> None:
60
+ path = self._token_path()
61
+ try:
62
+ data = json.loads(path.read_text())
63
+ except FileNotFoundError:
64
+ data = {}
65
+ except json.JSONDecodeError as exc:
66
+ raise QuiltAuthError("Token store contains invalid JSON.") from exc
67
+ except OSError as exc:
68
+ raise QuiltAuthError("Failed to read token store.") from exc
69
+ data[email] = asdict(tokens)
70
+ try:
71
+ path.write_text(json.dumps(data, indent=2))
72
+ os.chmod(path, 0o600)
73
+ except OSError as exc:
74
+ raise QuiltAuthError("Failed to persist token store.") from exc
75
+
76
+ def clear_tokens(self, email: str) -> None:
77
+ """Remove cached tokens for *email*."""
78
+ path = self._token_path()
79
+ try:
80
+ data = json.loads(path.read_text())
81
+ except FileNotFoundError:
82
+ return
83
+ except json.JSONDecodeError as exc:
84
+ raise QuiltAuthError("Token store contains invalid JSON.") from exc
85
+ except OSError as exc:
86
+ raise QuiltAuthError("Failed to read token store.") from exc
87
+
88
+ data.pop(email, None)
89
+ try:
90
+ path.write_text(json.dumps(data, indent=2))
91
+ os.chmod(path, 0o600)
92
+ except OSError as exc:
93
+ raise QuiltAuthError("Failed to persist token store.") from exc
94
+
95
+ def list_emails(self) -> list[str]:
96
+ """All email addresses that have cached tokens."""
97
+ try:
98
+ data = json.loads(self._token_path().read_text())
99
+ except FileNotFoundError:
100
+ return []
101
+ except json.JSONDecodeError as exc:
102
+ raise QuiltAuthError("Token store contains invalid JSON.") from exc
103
+ except OSError as exc:
104
+ raise QuiltAuthError("Failed to read token store.") from exc
105
+ return [k for k in data if isinstance(k, str)]