foodforthought-cli 0.2.7__tar.gz → 0.3.0__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 (176) hide show
  1. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/PKG-INFO +9 -1
  2. foodforthought_cli-0.3.0/ate/__init__.py +10 -0
  3. foodforthought_cli-0.3.0/ate/__main__.py +16 -0
  4. foodforthought_cli-0.3.0/ate/auth/__init__.py +1 -0
  5. foodforthought_cli-0.3.0/ate/auth/device_flow.py +141 -0
  6. foodforthought_cli-0.3.0/ate/auth/token_store.py +96 -0
  7. foodforthought_cli-0.3.0/ate/behaviors/__init__.py +100 -0
  8. foodforthought_cli-0.3.0/ate/behaviors/approach.py +399 -0
  9. foodforthought_cli-0.3.0/ate/behaviors/common.py +686 -0
  10. foodforthought_cli-0.3.0/ate/behaviors/tree.py +454 -0
  11. foodforthought_cli-0.3.0/ate/cli.py +1008 -0
  12. foodforthought_cli-0.3.0/ate/client.py +90 -0
  13. foodforthought_cli-0.3.0/ate/commands/__init__.py +168 -0
  14. foodforthought_cli-0.3.0/ate/commands/auth.py +389 -0
  15. foodforthought_cli-0.3.0/ate/commands/bridge.py +448 -0
  16. foodforthought_cli-0.3.0/ate/commands/data.py +185 -0
  17. foodforthought_cli-0.3.0/ate/commands/deps.py +111 -0
  18. foodforthought_cli-0.3.0/ate/commands/generate.py +384 -0
  19. foodforthought_cli-0.3.0/ate/commands/memory.py +907 -0
  20. foodforthought_cli-0.3.0/ate/commands/parts.py +166 -0
  21. foodforthought_cli-0.3.0/ate/commands/primitive.py +399 -0
  22. foodforthought_cli-0.3.0/ate/commands/protocol.py +288 -0
  23. foodforthought_cli-0.3.0/ate/commands/recording.py +524 -0
  24. foodforthought_cli-0.3.0/ate/commands/repo.py +154 -0
  25. foodforthought_cli-0.3.0/ate/commands/simulation.py +291 -0
  26. foodforthought_cli-0.3.0/ate/commands/skill.py +303 -0
  27. foodforthought_cli-0.3.0/ate/commands/skills.py +487 -0
  28. foodforthought_cli-0.3.0/ate/commands/team.py +147 -0
  29. foodforthought_cli-0.3.0/ate/commands/workflow.py +271 -0
  30. foodforthought_cli-0.3.0/ate/detection/__init__.py +38 -0
  31. foodforthought_cli-0.3.0/ate/detection/base.py +142 -0
  32. foodforthought_cli-0.3.0/ate/detection/color_detector.py +399 -0
  33. foodforthought_cli-0.3.0/ate/detection/trash_detector.py +322 -0
  34. foodforthought_cli-0.3.0/ate/drivers/__init__.py +39 -0
  35. foodforthought_cli-0.3.0/ate/drivers/ble_transport.py +405 -0
  36. foodforthought_cli-0.3.0/ate/drivers/mechdog.py +942 -0
  37. foodforthought_cli-0.3.0/ate/drivers/wifi_camera.py +477 -0
  38. foodforthought_cli-0.3.0/ate/interfaces/__init__.py +187 -0
  39. foodforthought_cli-0.3.0/ate/interfaces/base.py +273 -0
  40. foodforthought_cli-0.3.0/ate/interfaces/body.py +267 -0
  41. foodforthought_cli-0.3.0/ate/interfaces/detection.py +282 -0
  42. foodforthought_cli-0.3.0/ate/interfaces/locomotion.py +422 -0
  43. foodforthought_cli-0.3.0/ate/interfaces/manipulation.py +408 -0
  44. foodforthought_cli-0.3.0/ate/interfaces/navigation.py +389 -0
  45. foodforthought_cli-0.3.0/ate/interfaces/perception.py +362 -0
  46. foodforthought_cli-0.3.0/ate/interfaces/sensors.py +247 -0
  47. foodforthought_cli-0.3.0/ate/interfaces/types.py +371 -0
  48. foodforthought_cli-0.3.0/ate/llm_proxy.py +239 -0
  49. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/mcp_server.py +387 -0
  50. foodforthought_cli-0.3.0/ate/memory/__init__.py +35 -0
  51. foodforthought_cli-0.3.0/ate/memory/cloud.py +244 -0
  52. foodforthought_cli-0.3.0/ate/memory/context.py +269 -0
  53. foodforthought_cli-0.3.0/ate/memory/embeddings.py +184 -0
  54. foodforthought_cli-0.3.0/ate/memory/export.py +26 -0
  55. foodforthought_cli-0.3.0/ate/memory/merge.py +146 -0
  56. foodforthought_cli-0.3.0/ate/memory/migrate/__init__.py +34 -0
  57. foodforthought_cli-0.3.0/ate/memory/migrate/base.py +89 -0
  58. foodforthought_cli-0.3.0/ate/memory/migrate/pipeline.py +189 -0
  59. foodforthought_cli-0.3.0/ate/memory/migrate/sources/__init__.py +13 -0
  60. foodforthought_cli-0.3.0/ate/memory/migrate/sources/chroma.py +170 -0
  61. foodforthought_cli-0.3.0/ate/memory/migrate/sources/pinecone.py +120 -0
  62. foodforthought_cli-0.3.0/ate/memory/migrate/sources/qdrant.py +110 -0
  63. foodforthought_cli-0.3.0/ate/memory/migrate/sources/weaviate.py +160 -0
  64. foodforthought_cli-0.3.0/ate/memory/reranker.py +353 -0
  65. foodforthought_cli-0.3.0/ate/memory/search.py +26 -0
  66. foodforthought_cli-0.3.0/ate/memory/store.py +548 -0
  67. foodforthought_cli-0.3.0/ate/recording/__init__.py +83 -0
  68. foodforthought_cli-0.3.0/ate/recording/demonstration.py +378 -0
  69. foodforthought_cli-0.3.0/ate/recording/session.py +415 -0
  70. foodforthought_cli-0.3.0/ate/recording/upload.py +304 -0
  71. foodforthought_cli-0.3.0/ate/recording/visual.py +416 -0
  72. foodforthought_cli-0.3.0/ate/recording/wrapper.py +95 -0
  73. foodforthought_cli-0.3.0/ate/robot/__init__.py +221 -0
  74. foodforthought_cli-0.3.0/ate/robot/agentic_servo.py +856 -0
  75. foodforthought_cli-0.3.0/ate/robot/behaviors.py +493 -0
  76. foodforthought_cli-0.3.0/ate/robot/ble_capture.py +1000 -0
  77. foodforthought_cli-0.3.0/ate/robot/ble_enumerate.py +506 -0
  78. foodforthought_cli-0.3.0/ate/robot/calibration.py +668 -0
  79. foodforthought_cli-0.3.0/ate/robot/calibration_state.py +388 -0
  80. foodforthought_cli-0.3.0/ate/robot/commands.py +3735 -0
  81. foodforthought_cli-0.3.0/ate/robot/direction_calibration.py +554 -0
  82. foodforthought_cli-0.3.0/ate/robot/discovery.py +441 -0
  83. foodforthought_cli-0.3.0/ate/robot/introspection.py +330 -0
  84. foodforthought_cli-0.3.0/ate/robot/llm_system_id.py +654 -0
  85. foodforthought_cli-0.3.0/ate/robot/locomotion_calibration.py +508 -0
  86. foodforthought_cli-0.3.0/ate/robot/manager.py +270 -0
  87. foodforthought_cli-0.3.0/ate/robot/marker_generator.py +611 -0
  88. foodforthought_cli-0.3.0/ate/robot/perception.py +502 -0
  89. foodforthought_cli-0.3.0/ate/robot/primitives.py +614 -0
  90. foodforthought_cli-0.3.0/ate/robot/profiles.py +281 -0
  91. foodforthought_cli-0.3.0/ate/robot/registry.py +322 -0
  92. foodforthought_cli-0.3.0/ate/robot/servo_mapper.py +1153 -0
  93. foodforthought_cli-0.3.0/ate/robot/skill_upload.py +675 -0
  94. foodforthought_cli-0.3.0/ate/robot/target_calibration.py +500 -0
  95. foodforthought_cli-0.3.0/ate/robot/teach.py +515 -0
  96. foodforthought_cli-0.3.0/ate/robot/types.py +242 -0
  97. foodforthought_cli-0.3.0/ate/robot/visual_labeler.py +1048 -0
  98. foodforthought_cli-0.3.0/ate/robot/visual_servo_loop.py +494 -0
  99. foodforthought_cli-0.3.0/ate/robot/visual_servoing.py +570 -0
  100. foodforthought_cli-0.3.0/ate/robot/visual_system_id.py +906 -0
  101. foodforthought_cli-0.3.0/ate/transports/__init__.py +121 -0
  102. foodforthought_cli-0.3.0/ate/transports/base.py +394 -0
  103. foodforthought_cli-0.3.0/ate/transports/ble.py +405 -0
  104. foodforthought_cli-0.3.0/ate/transports/hybrid.py +444 -0
  105. foodforthought_cli-0.3.0/ate/transports/serial.py +345 -0
  106. foodforthought_cli-0.3.0/ate/urdf/__init__.py +30 -0
  107. foodforthought_cli-0.3.0/ate/urdf/capture.py +582 -0
  108. foodforthought_cli-0.3.0/ate/urdf/cloud.py +491 -0
  109. foodforthought_cli-0.3.0/ate/urdf/collision.py +271 -0
  110. foodforthought_cli-0.3.0/ate/urdf/commands.py +708 -0
  111. foodforthought_cli-0.3.0/ate/urdf/depth.py +360 -0
  112. foodforthought_cli-0.3.0/ate/urdf/inertial.py +312 -0
  113. foodforthought_cli-0.3.0/ate/urdf/kinematics.py +330 -0
  114. foodforthought_cli-0.3.0/ate/urdf/lifting.py +415 -0
  115. foodforthought_cli-0.3.0/ate/urdf/meshing.py +300 -0
  116. foodforthought_cli-0.3.0/ate/urdf/models/__init__.py +110 -0
  117. foodforthought_cli-0.3.0/ate/urdf/models/depth_anything.py +253 -0
  118. foodforthought_cli-0.3.0/ate/urdf/models/sam2.py +324 -0
  119. foodforthought_cli-0.3.0/ate/urdf/motion_analysis.py +396 -0
  120. foodforthought_cli-0.3.0/ate/urdf/pipeline.py +468 -0
  121. foodforthought_cli-0.3.0/ate/urdf/scale.py +256 -0
  122. foodforthought_cli-0.3.0/ate/urdf/scan_session.py +411 -0
  123. foodforthought_cli-0.3.0/ate/urdf/segmentation.py +299 -0
  124. foodforthought_cli-0.3.0/ate/urdf/synthesis.py +319 -0
  125. foodforthought_cli-0.3.0/ate/urdf/topology.py +336 -0
  126. foodforthought_cli-0.3.0/ate/urdf/validation.py +371 -0
  127. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/foodforthought_cli.egg-info/PKG-INFO +9 -1
  128. foodforthought_cli-0.3.0/foodforthought_cli.egg-info/SOURCES.txt +170 -0
  129. foodforthought_cli-0.3.0/foodforthought_cli.egg-info/requires.txt +19 -0
  130. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/setup.py +11 -1
  131. foodforthought_cli-0.2.7/ate/__init__.py +0 -4
  132. foodforthought_cli-0.2.7/ate/cli.py +0 -4148
  133. foodforthought_cli-0.2.7/foodforthought_cli.egg-info/SOURCES.txt +0 -48
  134. foodforthought_cli-0.2.7/foodforthought_cli.egg-info/requires.txt +0 -9
  135. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/README.md +0 -0
  136. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/bridge_server.py +0 -0
  137. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/compatibility.py +0 -0
  138. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/generator.py +0 -0
  139. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/generators/__init__.py +0 -0
  140. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/generators/docker_generator.py +0 -0
  141. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/generators/hardware_config.py +0 -0
  142. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/generators/ros2_generator.py +0 -0
  143. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/generators/skill_generator.py +0 -0
  144. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/marketplace.py +0 -0
  145. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/primitives.py +0 -0
  146. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/robot_setup.py +0 -0
  147. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/skill_schema.py +0 -0
  148. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/telemetry/__init__.py +0 -0
  149. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/telemetry/cli.py +0 -0
  150. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/telemetry/collector.py +0 -0
  151. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/telemetry/context.py +0 -0
  152. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/telemetry/fleet_agent.py +0 -0
  153. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/telemetry/formats/__init__.py +0 -0
  154. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/telemetry/formats/hdf5_serializer.py +0 -0
  155. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/telemetry/formats/mcap_serializer.py +0 -0
  156. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/ate/telemetry/types.py +0 -0
  157. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/foodforthought_cli.egg-info/dependency_links.txt +0 -0
  158. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/foodforthought_cli.egg-info/entry_points.txt +0 -0
  159. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/foodforthought_cli.egg-info/top_level.txt +0 -0
  160. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/mechdog_labeled/__init__.py +0 -0
  161. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/mechdog_labeled/primitives.py +0 -0
  162. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/mechdog_labeled/servo_map.py +0 -0
  163. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/mechdog_output/__init__.py +0 -0
  164. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/mechdog_output/primitives.py +0 -0
  165. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/mechdog_output/servo_map.py +0 -0
  166. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/setup.cfg +0 -0
  167. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/test_autodetect/__init__.py +0 -0
  168. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/test_autodetect/primitives.py +0 -0
  169. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/test_autodetect/servo_map.py +0 -0
  170. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/test_full_auto/__init__.py +0 -0
  171. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/test_full_auto/primitives.py +0 -0
  172. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/test_full_auto/servo_map.py +0 -0
  173. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/test_smart_detect/__init__.py +0 -0
  174. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/test_smart_detect/primitives.py +0 -0
  175. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/test_smart_detect/servo_map.py +0 -0
  176. {foodforthought_cli-0.2.7 → foodforthought_cli-0.3.0}/tests/test_auth.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: foodforthought-cli
3
- Version: 0.2.7
3
+ Version: 0.3.0
4
4
  Summary: CLI tool for FoodforThought robotics repository platform - manage robot skills and data
5
5
  Home-page: https://kindly.fyi/foodforthought
6
6
  Author: Kindly Robotics
@@ -28,9 +28,17 @@ Requires-Dist: requests>=2.28.0
28
28
  Provides-Extra: robot-setup
29
29
  Requires-Dist: pyserial>=3.5; extra == "robot-setup"
30
30
  Requires-Dist: anthropic>=0.18.0; extra == "robot-setup"
31
+ Provides-Extra: detection
32
+ Requires-Dist: Pillow>=9.0.0; extra == "detection"
33
+ Provides-Extra: visual-labeling
34
+ Requires-Dist: pyserial>=3.5; extra == "visual-labeling"
35
+ Requires-Dist: opencv-python>=4.5.0; extra == "visual-labeling"
36
+ Requires-Dist: Pillow>=9.0.0; extra == "visual-labeling"
31
37
  Provides-Extra: all
32
38
  Requires-Dist: pyserial>=3.5; extra == "all"
33
39
  Requires-Dist: anthropic>=0.18.0; extra == "all"
40
+ Requires-Dist: Pillow>=9.0.0; extra == "all"
41
+ Requires-Dist: opencv-python>=4.5.0; extra == "all"
34
42
  Dynamic: author
35
43
  Dynamic: author-email
36
44
  Dynamic: classifier
@@ -0,0 +1,10 @@
1
+ """FoodforThought CLI (ATE) - GitHub-like interface for robotics repositories"""
2
+
3
+ __version__ = "0.2.7"
4
+
5
+ # LLM Proxy for metered AI access
6
+ try:
7
+ from .llm_proxy import LLMProxy, LLMProxyError, LLMResponse, get_proxy
8
+ except ImportError:
9
+ pass # Optional dependency
10
+
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enable running ate as a module: python -m ate
4
+
5
+ This allows the CLI to be invoked via:
6
+ python -m ate --help
7
+ python -m ate robot upload mechdog --dry-run
8
+ python -m ate login
9
+
10
+ Instead of requiring the installed entry point.
11
+ """
12
+
13
+ from ate.cli import main
14
+
15
+ if __name__ == "__main__":
16
+ main()
@@ -0,0 +1 @@
1
+ """OAuth 2.0 Device Authorization Grant (RFC 8628) auth package."""
@@ -0,0 +1,141 @@
1
+ """
2
+ OAuth 2.0 Device Authorization Grant (RFC 8628) client.
3
+
4
+ Provides agent-friendly authentication for the `ate` CLI.
5
+ The device flow allows headless/CLI clients to authenticate
6
+ by having the user authorize on a separate device with a browser.
7
+ """
8
+
9
+ import time
10
+ from dataclasses import dataclass
11
+
12
+ import requests
13
+
14
+
15
+ class DeviceFlowError(Exception):
16
+ """General device flow error."""
17
+ pass
18
+
19
+
20
+ class DeviceFlowTimeout(DeviceFlowError):
21
+ """Polling timed out waiting for user authorization."""
22
+ pass
23
+
24
+
25
+ class DeviceFlowDenied(DeviceFlowError):
26
+ """User denied the authorization request."""
27
+ pass
28
+
29
+
30
+ @dataclass
31
+ class DeviceCodeResponse:
32
+ """Response from the device authorization endpoint."""
33
+ device_code: str
34
+ user_code: str # e.g. "ABCD-1234" — human types this
35
+ verification_uri: str # e.g. "https://kindly.fyi/device"
36
+ expires_in: int # seconds (default 600)
37
+ interval: int # polling interval seconds (default 5)
38
+
39
+
40
+ @dataclass
41
+ class TokenResponse:
42
+ """Token response from the token endpoint."""
43
+ access_token: str
44
+ refresh_token: str
45
+ expires_in: int # seconds
46
+ token_type: str # "Bearer"
47
+
48
+
49
+ class DeviceFlowClient:
50
+ """OAuth 2.0 Device Authorization Grant (RFC 8628) client."""
51
+
52
+ def __init__(self, server_url: str = "https://kindly.fyi"):
53
+ self.server_url = server_url
54
+
55
+ def request_code(self) -> DeviceCodeResponse:
56
+ """POST /api/auth/device/code → DeviceCodeResponse.
57
+
58
+ Raises DeviceFlowError on HTTP or network errors.
59
+ """
60
+ try:
61
+ resp = requests.post(
62
+ f"{self.server_url}/api/auth/device/code",
63
+ json={"client_id": "ate-cli"},
64
+ timeout=10,
65
+ )
66
+ resp.raise_for_status()
67
+ data = resp.json()
68
+ return DeviceCodeResponse(
69
+ device_code=data["device_code"],
70
+ user_code=data["user_code"],
71
+ verification_uri=data["verification_uri"],
72
+ expires_in=data["expires_in"],
73
+ interval=data["interval"],
74
+ )
75
+ except Exception as e:
76
+ if isinstance(e, DeviceFlowError):
77
+ raise
78
+ raise DeviceFlowError(f"Failed to request device code: {e}") from e
79
+
80
+ def poll_for_token(
81
+ self,
82
+ device_code: str,
83
+ interval: int = 5,
84
+ expires_in: int = 600,
85
+ ) -> TokenResponse:
86
+ """Poll POST /api/auth/device/token until authorized or expired.
87
+
88
+ Returns TokenResponse on success.
89
+ Raises DeviceFlowTimeout if expires_in exceeded.
90
+ Raises DeviceFlowDenied if user denied.
91
+ """
92
+ current_interval = interval
93
+ start_time = time.time()
94
+
95
+ while True:
96
+ time.sleep(current_interval)
97
+
98
+ elapsed = time.time() - start_time
99
+ if elapsed > expires_in:
100
+ raise DeviceFlowTimeout(
101
+ f"Device authorization timed out after {expires_in}s"
102
+ )
103
+
104
+ resp = requests.post(
105
+ f"{self.server_url}/api/auth/device/token",
106
+ json={
107
+ "client_id": "ate-cli",
108
+ "device_code": device_code,
109
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
110
+ },
111
+ timeout=10,
112
+ )
113
+
114
+ if resp.status_code == 200:
115
+ data = resp.json()
116
+ return TokenResponse(
117
+ access_token=data["access_token"],
118
+ refresh_token=data["refresh_token"],
119
+ expires_in=data["expires_in"],
120
+ token_type=data["token_type"],
121
+ )
122
+
123
+ # Handle error responses (400-level)
124
+ try:
125
+ error_data = resp.json()
126
+ except Exception:
127
+ continue
128
+
129
+ error = error_data.get("error", "")
130
+
131
+ if error == "authorization_pending":
132
+ continue
133
+ elif error == "slow_down":
134
+ current_interval += 5
135
+ continue
136
+ elif error == "access_denied":
137
+ raise DeviceFlowDenied("User denied the authorization request")
138
+ elif error == "expired_token":
139
+ raise DeviceFlowTimeout("Device code expired")
140
+ else:
141
+ continue
@@ -0,0 +1,96 @@
1
+ """
2
+ Token persistence for OAuth 2.0 device flow credentials.
3
+
4
+ Stores tokens at ~/.ate/credentials.json with expiry tracking.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import time
10
+ from typing import Optional
11
+
12
+ from ate.auth.device_flow import TokenResponse
13
+
14
+
15
+ class TokenStore:
16
+ """Manages token persistence at ~/.ate/credentials.json."""
17
+
18
+ def __init__(self, path: str = None):
19
+ self.path = path or os.path.expanduser("~/.ate/credentials.json")
20
+
21
+ def save(self, token_response: TokenResponse) -> None:
22
+ """Save tokens to disk (creates parent dirs).
23
+
24
+ Records saved_at timestamp for expiry calculations.
25
+ """
26
+ parent = os.path.dirname(self.path)
27
+ if parent:
28
+ os.makedirs(parent, exist_ok=True)
29
+
30
+ data = {
31
+ "access_token": token_response.access_token,
32
+ "refresh_token": token_response.refresh_token,
33
+ "expires_in": token_response.expires_in,
34
+ "token_type": token_response.token_type,
35
+ "saved_at": time.time(),
36
+ }
37
+
38
+ with open(self.path, "w") as f:
39
+ json.dump(data, f, indent=2)
40
+
41
+ def load(self) -> Optional[TokenResponse]:
42
+ """Load tokens from disk. Returns None if no credentials or invalid."""
43
+ if not os.path.exists(self.path):
44
+ return None
45
+
46
+ try:
47
+ with open(self.path) as f:
48
+ data = json.load(f)
49
+ return TokenResponse(
50
+ access_token=data["access_token"],
51
+ refresh_token=data["refresh_token"],
52
+ expires_in=data["expires_in"],
53
+ token_type=data["token_type"],
54
+ )
55
+ except (json.JSONDecodeError, KeyError, TypeError):
56
+ return None
57
+
58
+ def clear(self) -> None:
59
+ """Delete stored credentials."""
60
+ try:
61
+ os.remove(self.path)
62
+ except FileNotFoundError:
63
+ pass
64
+
65
+ def _load_raw(self) -> Optional[dict]:
66
+ """Load raw JSON data including saved_at."""
67
+ if not os.path.exists(self.path):
68
+ return None
69
+ try:
70
+ with open(self.path) as f:
71
+ return json.load(f)
72
+ except (json.JSONDecodeError, TypeError):
73
+ return None
74
+
75
+ def _expires_at(self) -> Optional[float]:
76
+ """Calculate the absolute expiry time."""
77
+ data = self._load_raw()
78
+ if data is None:
79
+ return None
80
+ saved_at = data.get("saved_at", 0)
81
+ expires_in = data.get("expires_in", 0)
82
+ return saved_at + expires_in
83
+
84
+ def is_expired(self) -> bool:
85
+ """Check if the access token has expired."""
86
+ expires_at = self._expires_at()
87
+ if expires_at is None:
88
+ return True
89
+ return time.time() >= expires_at
90
+
91
+ def needs_refresh(self) -> bool:
92
+ """Check if token is within 5 minutes of expiry."""
93
+ expires_at = self._expires_at()
94
+ if expires_at is None:
95
+ return True
96
+ return time.time() >= (expires_at - 300)
@@ -0,0 +1,100 @@
1
+ """
2
+ Behavior Tree framework for composing robot skills.
3
+
4
+ Behavior trees allow complex behaviors to be built from simple skills
5
+ using a tree structure of:
6
+ - Sequences (do A, then B, then C)
7
+ - Selectors (try A, if fails try B)
8
+ - Conditions (if X is true...)
9
+ - Actions (do X)
10
+
11
+ This is how we turn small demos into valuable composite behaviors.
12
+ """
13
+
14
+ from .tree import (
15
+ BehaviorNode,
16
+ BehaviorStatus,
17
+ Sequence,
18
+ Selector,
19
+ Parallel,
20
+ Action,
21
+ Condition,
22
+ Inverter,
23
+ Succeeder,
24
+ Repeater,
25
+ RepeatUntilFail,
26
+ BehaviorTree,
27
+ )
28
+
29
+ from .common import (
30
+ # Navigation
31
+ NavigateToPoint,
32
+ NavigateToPose,
33
+ Patrol,
34
+ ReturnHome,
35
+ # Detection
36
+ DetectObject,
37
+ IsObjectVisible,
38
+ FindNearest,
39
+ ApproachObject,
40
+ # Manipulation
41
+ PickUp,
42
+ PlaceAt,
43
+ DropInBin,
44
+ # Conditions
45
+ IsBatteryLow,
46
+ IsPathClear,
47
+ HasObject,
48
+ # Composite behaviors
49
+ PatrolAndCleanup,
50
+ SearchAndRetrieve,
51
+ )
52
+
53
+ from .approach import (
54
+ ApproachState,
55
+ ApproachConfig,
56
+ ApproachTarget,
57
+ VisualServoApproach,
58
+ )
59
+
60
+ __all__ = [
61
+ # Core tree nodes
62
+ "BehaviorNode",
63
+ "BehaviorStatus",
64
+ "Sequence",
65
+ "Selector",
66
+ "Parallel",
67
+ "Action",
68
+ "Condition",
69
+ "Inverter",
70
+ "Succeeder",
71
+ "Repeater",
72
+ "RepeatUntilFail",
73
+ "BehaviorTree",
74
+ # Navigation actions
75
+ "NavigateToPoint",
76
+ "NavigateToPose",
77
+ "Patrol",
78
+ "ReturnHome",
79
+ # Detection actions
80
+ "DetectObject",
81
+ "IsObjectVisible",
82
+ "FindNearest",
83
+ "ApproachObject",
84
+ # Manipulation actions
85
+ "PickUp",
86
+ "PlaceAt",
87
+ "DropInBin",
88
+ # Conditions
89
+ "IsBatteryLow",
90
+ "IsPathClear",
91
+ "HasObject",
92
+ # Composite behaviors
93
+ "PatrolAndCleanup",
94
+ "SearchAndRetrieve",
95
+ # Approach behaviors (locomotion-agnostic)
96
+ "ApproachState",
97
+ "ApproachConfig",
98
+ "ApproachTarget",
99
+ "VisualServoApproach",
100
+ ]