watcherobot 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 (92) hide show
  1. watcherobot/__init__.py +83 -0
  2. watcherobot/_internal/__init__.py +1 -0
  3. watcherobot/_internal/audio_status.py +40 -0
  4. watcherobot/application/__init__.py +30 -0
  5. watcherobot/application/channels.py +35 -0
  6. watcherobot/application/context.py +99 -0
  7. watcherobot/application/desktop.py +26 -0
  8. watcherobot/application/ignore.py +54 -0
  9. watcherobot/application/project.py +336 -0
  10. watcherobot/application/rtc.py +402 -0
  11. watcherobot/application/transport.py +533 -0
  12. watcherobot/audio.py +81 -0
  13. watcherobot/cli.py +1186 -0
  14. watcherobot/distribution/__init__.py +154 -0
  15. watcherobot/distribution/catalog_submission.py +176 -0
  16. watcherobot/distribution/check.py +66 -0
  17. watcherobot/distribution/cli.py +1077 -0
  18. watcherobot/distribution/credentials.py +85 -0
  19. watcherobot/distribution/download.py +242 -0
  20. watcherobot/distribution/events.py +172 -0
  21. watcherobot/distribution/hf_marketplace.py +286 -0
  22. watcherobot/distribution/hf_publish.py +351 -0
  23. watcherobot/distribution/hub_http.py +128 -0
  24. watcherobot/distribution/install.py +1010 -0
  25. watcherobot/distribution/login.py +318 -0
  26. watcherobot/distribution/marketplace.py +228 -0
  27. watcherobot/distribution/oauth_http.py +229 -0
  28. watcherobot/distribution/ports.py +327 -0
  29. watcherobot/distribution/publish.py +205 -0
  30. watcherobot/distribution/publish_files.py +140 -0
  31. watcherobot/distribution/source_files.py +107 -0
  32. watcherobot/distribution/submit.py +398 -0
  33. watcherobot/errors.py +50 -0
  34. watcherobot/inputs.py +180 -0
  35. watcherobot/job.py +115 -0
  36. watcherobot/media.py +243 -0
  37. watcherobot/opus.py +79 -0
  38. watcherobot/protocol.py +95 -0
  39. watcherobot/provisioning/__init__.py +41 -0
  40. watcherobot/provisioning/backend.py +50 -0
  41. watcherobot/provisioning/bleak_backend.py +271 -0
  42. watcherobot/provisioning/errors.py +82 -0
  43. watcherobot/provisioning/models.py +102 -0
  44. watcherobot/provisioning/protocol.py +190 -0
  45. watcherobot/provisioning/service.py +526 -0
  46. watcherobot/py.typed +0 -0
  47. watcherobot/robot.py +1093 -0
  48. watcherobot/runtime/__init__.py +1 -0
  49. watcherobot/runtime/daemon/__init__.py +16 -0
  50. watcherobot/runtime/daemon/__main__.py +159 -0
  51. watcherobot/runtime/daemon/application/__init__.py +44 -0
  52. watcherobot/runtime/daemon/application/bridge.py +237 -0
  53. watcherobot/runtime/daemon/application/client.py +140 -0
  54. watcherobot/runtime/daemon/application/launcher.py +396 -0
  55. watcherobot/runtime/daemon/application/logging.py +89 -0
  56. watcherobot/runtime/daemon/application/manifest.py +352 -0
  57. watcherobot/runtime/daemon/application/runtime.py +551 -0
  58. watcherobot/runtime/daemon/application/session.py +146 -0
  59. watcherobot/runtime/daemon/connections/__init__.py +39 -0
  60. watcherobot/runtime/daemon/connections/device_state.py +41 -0
  61. watcherobot/runtime/daemon/connections/identity.py +95 -0
  62. watcherobot/runtime/daemon/connections/registry.py +134 -0
  63. watcherobot/runtime/daemon/connections/websocket_server.py +482 -0
  64. watcherobot/runtime/daemon/control/__init__.py +5 -0
  65. watcherobot/runtime/daemon/control/rest.py +641 -0
  66. watcherobot/runtime/daemon/instance.py +146 -0
  67. watcherobot/runtime/daemon/logging.py +90 -0
  68. watcherobot/runtime/daemon/maintenance/__init__.py +5 -0
  69. watcherobot/runtime/daemon/maintenance/card_reader.py +906 -0
  70. watcherobot/runtime/daemon/maintenance/releases.py +426 -0
  71. watcherobot/runtime/daemon/maintenance/service.py +1475 -0
  72. watcherobot/runtime/daemon/maintenance/work_assets.py +241 -0
  73. watcherobot/runtime/daemon/maintenance/works.py +722 -0
  74. watcherobot/runtime/daemon/pairing/__init__.py +14 -0
  75. watcherobot/runtime/daemon/pairing/protocol.py +434 -0
  76. watcherobot/runtime/daemon/pairing/session.py +302 -0
  77. watcherobot/runtime/daemon/pairing/udp.py +659 -0
  78. watcherobot/runtime/daemon/preview/__init__.py +5 -0
  79. watcherobot/runtime/daemon/preview/face_tracking.py +113 -0
  80. watcherobot/runtime/daemon/preview/udp_feedback.py +67 -0
  81. watcherobot/runtime/daemon/preview/udp_protocol.py +276 -0
  82. watcherobot/runtime/daemon/preview/udp_service.py +214 -0
  83. watcherobot/runtime/daemon/routing/__init__.py +5 -0
  84. watcherobot/runtime/daemon/routing/raw.py +93 -0
  85. watcherobot/runtime/daemon/runtime.py +543 -0
  86. watcherobot/runtime/frozen_entry.py +32 -0
  87. watcherobot/vision.py +523 -0
  88. watcherobot-0.1.1.dist-info/METADATA +273 -0
  89. watcherobot-0.1.1.dist-info/RECORD +92 -0
  90. watcherobot-0.1.1.dist-info/WHEEL +4 -0
  91. watcherobot-0.1.1.dist-info/entry_points.txt +3 -0
  92. watcherobot-0.1.1.dist-info/licenses/LICENSE +177 -0
@@ -0,0 +1,83 @@
1
+ from .errors import (
2
+ AuthenticationError,
3
+ CommandError,
4
+ ConnectionTimeoutError,
5
+ JobCancelledError,
6
+ JobFailedError,
7
+ WatcheRobotError,
8
+ )
9
+ from .audio import AudioPlayback, PCMAudio
10
+ from .job import Job, JobState
11
+ from .inputs import BackTouchEvent, InputDomain, InputEvent, RollerEvent, ScreenTouchEvent
12
+ from .media import AudioFormat, AudioFrame, AudioRecording, ImageFrame, MicrophoneSession
13
+ from .provisioning import (
14
+ BluetoothConnectionTimeoutError,
15
+ BluetoothDevice,
16
+ BluetoothPermissionError,
17
+ BluetoothProvisioner,
18
+ BluetoothProvisioningError,
19
+ BluetoothUnavailableError,
20
+ DeviceAmbiguityError,
21
+ DeviceNotFoundError,
22
+ PayloadTooLargeError,
23
+ ProtocolMessage,
24
+ ProvisioningCancelledError,
25
+ ProvisioningProtocolError,
26
+ ProvisioningRejectedError,
27
+ ProvisioningResponseTimeoutError,
28
+ ProvisioningResult,
29
+ WifiStatus,
30
+ )
31
+ from .vision import (
32
+ FaceBox,
33
+ FaceTrackingFrame,
34
+ FaceTrackingPreview,
35
+ FaceTrackingStopPolicy,
36
+ FaceTrackingTelemetry,
37
+ )
38
+
39
+ __all__ = [
40
+ "AudioFormat",
41
+ "AudioFrame",
42
+ "AudioPlayback",
43
+ "AudioRecording",
44
+ "AuthenticationError",
45
+ "BackTouchEvent",
46
+ "BluetoothConnectionTimeoutError",
47
+ "BluetoothDevice",
48
+ "BluetoothPermissionError",
49
+ "BluetoothProvisioner",
50
+ "BluetoothProvisioningError",
51
+ "BluetoothUnavailableError",
52
+ "CommandError",
53
+ "ConnectionTimeoutError",
54
+ "DeviceAmbiguityError",
55
+ "DeviceNotFoundError",
56
+ "FaceBox",
57
+ "FaceTrackingFrame",
58
+ "FaceTrackingPreview",
59
+ "FaceTrackingStopPolicy",
60
+ "FaceTrackingTelemetry",
61
+ "ImageFrame",
62
+ "InputDomain",
63
+ "InputEvent",
64
+ "Job",
65
+ "JobCancelledError",
66
+ "JobFailedError",
67
+ "JobState",
68
+ "MicrophoneSession",
69
+ "PCMAudio",
70
+ "PayloadTooLargeError",
71
+ "ProtocolMessage",
72
+ "ProvisioningCancelledError",
73
+ "ProvisioningProtocolError",
74
+ "ProvisioningRejectedError",
75
+ "ProvisioningResponseTimeoutError",
76
+ "ProvisioningResult",
77
+ "RollerEvent",
78
+ "ScreenTouchEvent",
79
+ "WatcheRobotError",
80
+ "WifiStatus",
81
+ ]
82
+
83
+ __version__ = "0.1.1"
@@ -0,0 +1 @@
1
+ """Private implementation helpers for the WatcheRobot SDK."""
@@ -0,0 +1,40 @@
1
+ """Single source of truth for device audio stream status semantics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class AudioStatusKind(str, Enum):
9
+ RUNNING = "running"
10
+ COMPLETED = "completed"
11
+ FAILED = "failed"
12
+ CANCELLED = "cancelled"
13
+
14
+
15
+ FAILED_AUDIO_REASONS = frozenset(
16
+ {
17
+ "enqueue_timeout",
18
+ "enqueue_failed",
19
+ "queue_lock_timeout",
20
+ "sequence_gap",
21
+ "stale_stream",
22
+ "stale_frame",
23
+ "playback_start_failed",
24
+ "playback_write_failed",
25
+ }
26
+ )
27
+
28
+ CANCELLED_AUDIO_REASONS = frozenset({"aborted", "replaced_by_new_stream"})
29
+
30
+
31
+ def classify_audio_status(reason: object) -> AudioStatusKind:
32
+ """Map one protocol reason to the public playback lifecycle."""
33
+
34
+ if reason == "complete":
35
+ return AudioStatusKind.COMPLETED
36
+ if reason in FAILED_AUDIO_REASONS:
37
+ return AudioStatusKind.FAILED
38
+ if reason in CANCELLED_AUDIO_REASONS:
39
+ return AudioStatusKind.CANCELLED
40
+ return AudioStatusKind.RUNNING
@@ -0,0 +1,30 @@
1
+ """Public API for Daemon-managed WatcheRobot Applications."""
2
+
3
+ from .channels import (
4
+ ApplicationChannel,
5
+ ApplicationChannels,
6
+ Frame,
7
+ FrameCallback,
8
+ )
9
+ from .context import ApplicationContext, ApplicationEnvironmentError
10
+ from .rtc import (
11
+ ApplicationRtc,
12
+ RTC_AUDIO_CAPABILITY,
13
+ RTC_PROTOCOL,
14
+ RTC_VIDEO_CAPABILITY,
15
+ RtcSessionRejectedError,
16
+ )
17
+
18
+ __all__ = [
19
+ "ApplicationChannel",
20
+ "ApplicationChannels",
21
+ "ApplicationContext",
22
+ "ApplicationEnvironmentError",
23
+ "ApplicationRtc",
24
+ "Frame",
25
+ "FrameCallback",
26
+ "RTC_AUDIO_CAPABILITY",
27
+ "RTC_PROTOCOL",
28
+ "RTC_VIDEO_CAPABILITY",
29
+ "RtcSessionRejectedError",
30
+ ]
@@ -0,0 +1,35 @@
1
+ """Low-level channels for advanced Daemon-managed Applications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from watcherobot.runtime.daemon.application.client import (
6
+ ApplicationCommunicators,
7
+ Frame,
8
+ FrameCallback,
9
+ )
10
+ from watcherobot.runtime.daemon.application.session import ApplicationChannel
11
+
12
+
13
+ class ApplicationChannels(ApplicationCommunicators):
14
+ """Expose source-aware desktop/device frames to an advanced Application.
15
+
16
+ Most Applications should use :class:`ApplicationContext`. This lower-level
17
+ API exists for Applications such as ``watcher_default`` that already own a
18
+ complete business protocol stack and need the original frames unchanged.
19
+ """
20
+
21
+ @property
22
+ def desktop_url(self) -> str:
23
+ return self._desktop_url
24
+
25
+ @property
26
+ def device_url(self) -> str:
27
+ return self._device_url
28
+
29
+
30
+ __all__ = [
31
+ "ApplicationChannel",
32
+ "ApplicationChannels",
33
+ "Frame",
34
+ "FrameCallback",
35
+ ]
@@ -0,0 +1,99 @@
1
+ """Public context for a Daemon-managed WatcheRobot Application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ import sys
9
+ from types import TracebackType
10
+
11
+ from watcherobot.application.desktop import ApplicationDesktop
12
+ from watcherobot.application.rtc import ApplicationRtc
13
+ from watcherobot.application.transport import DaemonApplicationTransport
14
+ from watcherobot.robot import WatcheRobot
15
+
16
+
17
+ class ApplicationEnvironmentError(RuntimeError):
18
+ """Raised when code is not running inside an authorized Application."""
19
+
20
+
21
+ class ApplicationContext:
22
+ """Own the SDK objects injected into one managed Application process."""
23
+
24
+ def __init__(
25
+ self,
26
+ *,
27
+ app_id: str,
28
+ transport: DaemonApplicationTransport,
29
+ ) -> None:
30
+ self.app_id = app_id
31
+ self._transport = transport
32
+ self.robot = WatcheRobot._from_transport(transport)
33
+ self.desktop = ApplicationDesktop(transport)
34
+ self.rtc = ApplicationRtc(transport)
35
+ self.logger = _build_application_logger(app_id)
36
+ self._entered = False
37
+
38
+ @classmethod
39
+ def from_environment(cls) -> "ApplicationContext":
40
+ required = {
41
+ "WATCHER_APP_ID": os.environ.get("WATCHER_APP_ID", "").strip(),
42
+ "WATCHER_APP_RUN_CREDENTIAL": os.environ.get(
43
+ "WATCHER_APP_RUN_CREDENTIAL",
44
+ "",
45
+ ).strip(),
46
+ "WATCHER_APP_DESKTOP_WS_URL": os.environ.get(
47
+ "WATCHER_APP_DESKTOP_WS_URL",
48
+ "",
49
+ ).strip(),
50
+ "WATCHER_APP_DEVICE_WS_URL": os.environ.get(
51
+ "WATCHER_APP_DEVICE_WS_URL",
52
+ "",
53
+ ).strip(),
54
+ }
55
+ missing = [name for name, value in required.items() if not value]
56
+ if missing:
57
+ raise ApplicationEnvironmentError(
58
+ "Application environment is incomplete: "
59
+ + ", ".join(sorted(missing))
60
+ )
61
+ return cls(
62
+ app_id=required["WATCHER_APP_ID"],
63
+ transport=DaemonApplicationTransport(),
64
+ )
65
+
66
+ async def __aenter__(self) -> "ApplicationContext":
67
+ if self._entered:
68
+ raise RuntimeError("ApplicationContext is already entered")
69
+ await asyncio.to_thread(self._transport.start)
70
+ self._entered = True
71
+ return self
72
+
73
+ async def __aexit__(
74
+ self,
75
+ exc_type: type[BaseException] | None,
76
+ exc_value: BaseException | None,
77
+ traceback: TracebackType | None,
78
+ ) -> None:
79
+ await asyncio.to_thread(self.close)
80
+
81
+ def close(self) -> None:
82
+ if not self._entered:
83
+ return
84
+ self.rtc.close()
85
+ self.robot.close()
86
+ self._entered = False
87
+
88
+
89
+ def _build_application_logger(app_id: str) -> logging.Logger:
90
+ logger = logging.getLogger(f"watcherobot.application.{app_id}")
91
+ logger.setLevel(logging.INFO)
92
+ logger.propagate = False
93
+ if not logger.handlers:
94
+ handler = logging.StreamHandler(sys.stdout)
95
+ handler.setFormatter(
96
+ logging.Formatter("%(asctime)s %(levelname)s %(message)s")
97
+ )
98
+ logger.addHandler(handler)
99
+ return logger
@@ -0,0 +1,26 @@
1
+ """Desktop-side Application channel exposed without device ownership."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import queue
7
+
8
+ from watcherobot.application.transport import DaemonApplicationTransport
9
+
10
+
11
+ class ApplicationDesktop:
12
+ """Send and receive raw desktop frames through the managed Daemon bridge."""
13
+
14
+ def __init__(self, transport: DaemonApplicationTransport) -> None:
15
+ self._transport = transport
16
+ self._frames: queue.Queue[str | bytes] = queue.Queue()
17
+ transport.set_desktop_callback(self._frames.put)
18
+
19
+ async def send(self, frame: str | bytes) -> None:
20
+ await asyncio.wrap_future(self._transport.send_desktop(frame))
21
+
22
+ async def receive(self, *, timeout: float | None = None) -> str | bytes:
23
+ try:
24
+ return await asyncio.to_thread(self._frames.get, True, timeout)
25
+ except queue.Empty as exc:
26
+ raise TimeoutError("Timed out waiting for a desktop frame") from exc
@@ -0,0 +1,54 @@
1
+ """Shared Application source ignore rules for validation and publishing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fnmatch import fnmatchcase
6
+ from pathlib import Path, PurePosixPath
7
+
8
+
9
+ APPLICATION_IGNORE_FILE = ".watcherignore"
10
+
11
+
12
+ class ApplicationIgnoreError(RuntimeError):
13
+ """Raised when Application ignore rules cannot be loaded safely."""
14
+
15
+
16
+ def load_application_ignore_patterns(source: Path) -> tuple[str, ...]:
17
+ """Load normalized non-comment patterns from one Application root."""
18
+
19
+ ignore_path = Path(source) / APPLICATION_IGNORE_FILE
20
+ if not ignore_path.is_file():
21
+ return ()
22
+ try:
23
+ lines = ignore_path.read_text(encoding="utf-8").splitlines()
24
+ except (OSError, UnicodeError) as exc:
25
+ raise ApplicationIgnoreError(
26
+ f"Cannot read {APPLICATION_IGNORE_FILE}: {exc}"
27
+ ) from exc
28
+ return tuple(
29
+ line.strip().replace("\\", "/")
30
+ for line in lines
31
+ if line.strip() and not line.lstrip().startswith("#")
32
+ )
33
+
34
+
35
+ def is_application_path_ignored(
36
+ relative_path: Path,
37
+ patterns: tuple[str, ...],
38
+ ) -> bool:
39
+ """Return whether a relative Application path matches ignore rules."""
40
+
41
+ relative = PurePosixPath(relative_path.as_posix())
42
+ for pattern in patterns:
43
+ if pattern.endswith("/"):
44
+ prefix = pattern.rstrip("/")
45
+ ancestors = (
46
+ "/".join(relative.parts[:depth])
47
+ for depth in range(1, len(relative.parts))
48
+ )
49
+ if any(fnmatchcase(ancestor, prefix) for ancestor in ancestors):
50
+ return True
51
+ continue
52
+ if relative.match(pattern):
53
+ return True
54
+ return False