luckyrobots 0.1.68__py3-none-any.whl → 0.1.69__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 (57) hide show
  1. luckyrobots/__init__.py +23 -12
  2. luckyrobots/client.py +800 -0
  3. luckyrobots/config/robots.yaml +231 -71
  4. luckyrobots/engine/__init__.py +23 -0
  5. luckyrobots/{utils → engine}/check_updates.py +108 -48
  6. luckyrobots/{utils → engine}/download.py +61 -39
  7. luckyrobots/engine/manager.py +427 -0
  8. luckyrobots/grpc/__init__.py +6 -0
  9. luckyrobots/grpc/generated/__init__.py +18 -0
  10. luckyrobots/grpc/generated/agent_pb2.py +61 -0
  11. luckyrobots/grpc/generated/agent_pb2_grpc.py +255 -0
  12. luckyrobots/grpc/generated/camera_pb2.py +45 -0
  13. luckyrobots/grpc/generated/camera_pb2_grpc.py +155 -0
  14. luckyrobots/grpc/generated/common_pb2.py +39 -0
  15. luckyrobots/grpc/generated/common_pb2_grpc.py +27 -0
  16. luckyrobots/grpc/generated/media_pb2.py +35 -0
  17. luckyrobots/grpc/generated/media_pb2_grpc.py +27 -0
  18. luckyrobots/grpc/generated/mujoco_pb2.py +47 -0
  19. luckyrobots/grpc/generated/mujoco_pb2_grpc.py +248 -0
  20. luckyrobots/grpc/generated/scene_pb2.py +54 -0
  21. luckyrobots/grpc/generated/scene_pb2_grpc.py +248 -0
  22. luckyrobots/grpc/generated/telemetry_pb2.py +43 -0
  23. luckyrobots/grpc/generated/telemetry_pb2_grpc.py +154 -0
  24. luckyrobots/grpc/generated/viewport_pb2.py +48 -0
  25. luckyrobots/grpc/generated/viewport_pb2_grpc.py +155 -0
  26. luckyrobots/grpc/proto/agent.proto +152 -0
  27. luckyrobots/grpc/proto/camera.proto +41 -0
  28. luckyrobots/grpc/proto/common.proto +36 -0
  29. luckyrobots/grpc/proto/hazel_rpc.proto +32 -0
  30. luckyrobots/grpc/proto/media.proto +26 -0
  31. luckyrobots/grpc/proto/mujoco.proto +64 -0
  32. luckyrobots/grpc/proto/scene.proto +70 -0
  33. luckyrobots/grpc/proto/telemetry.proto +43 -0
  34. luckyrobots/grpc/proto/viewport.proto +45 -0
  35. luckyrobots/luckyrobots.py +193 -0
  36. luckyrobots/models/__init__.py +13 -0
  37. luckyrobots/models/camera.py +97 -0
  38. luckyrobots/models/observation.py +135 -0
  39. luckyrobots/{utils/helpers.py → utils.py} +75 -40
  40. luckyrobots-0.1.69.dist-info/METADATA +262 -0
  41. luckyrobots-0.1.69.dist-info/RECORD +44 -0
  42. {luckyrobots-0.1.68.dist-info → luckyrobots-0.1.69.dist-info}/WHEEL +1 -1
  43. luckyrobots/core/luckyrobots.py +0 -628
  44. luckyrobots/core/manager.py +0 -236
  45. luckyrobots/core/models.py +0 -68
  46. luckyrobots/core/node.py +0 -273
  47. luckyrobots/message/__init__.py +0 -18
  48. luckyrobots/message/pubsub.py +0 -145
  49. luckyrobots/message/srv/client.py +0 -81
  50. luckyrobots/message/srv/service.py +0 -135
  51. luckyrobots/message/srv/types.py +0 -83
  52. luckyrobots/message/transporter.py +0 -427
  53. luckyrobots/utils/event_loop.py +0 -94
  54. luckyrobots/utils/sim_manager.py +0 -413
  55. luckyrobots-0.1.68.dist-info/METADATA +0 -253
  56. luckyrobots-0.1.68.dist-info/RECORD +0 -24
  57. {luckyrobots-0.1.68.dist-info → luckyrobots-0.1.69.dist-info}/licenses/LICENSE +0 -0
@@ -1,145 +0,0 @@
1
- """
2
- Publisher/subscriber implementation with WebSocket transport.
3
-
4
- This module provides Publisher and Subscriber classes for implementing
5
- publisher/subscriber patterns with WebSocket transport for distributed
6
- communication.
7
- """
8
-
9
- import logging
10
- import threading
11
- from typing import Any, Callable, Dict, List, Type
12
-
13
- logging.basicConfig(
14
- level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
15
- )
16
- logger = logging.getLogger("pubsub")
17
-
18
-
19
- class Publisher:
20
- # Class dictionary to keep track of all publishers by topic
21
- _publishers_by_topic: Dict[str, List["Publisher"]] = {}
22
- _lock = threading.RLock()
23
-
24
- def __init__(self, topic: str, message_type: Type, queue_size: int = 10):
25
- self.topic = topic
26
- self.message_type = message_type
27
- self.queue_size = queue_size
28
- self._subscribers: List[Callable[[Any], None]] = []
29
-
30
- # Register this publisher in the class dictionary
31
- with Publisher._lock:
32
- if topic not in Publisher._publishers_by_topic:
33
- Publisher._publishers_by_topic[topic] = []
34
- Publisher._publishers_by_topic[topic].append(self)
35
-
36
- logger.debug(f"Created publisher for topic: {topic}")
37
-
38
- def __del__(self):
39
- """Delete the publisher"""
40
- with Publisher._lock:
41
- if self.topic in Publisher._publishers_by_topic:
42
- if self in Publisher._publishers_by_topic[self.topic]:
43
- Publisher._publishers_by_topic[self.topic].remove(self)
44
- if not Publisher._publishers_by_topic[self.topic]:
45
- del Publisher._publishers_by_topic[self.topic]
46
-
47
- def publish(self, message: Any) -> None:
48
- # Type check the message
49
- if not isinstance(message, self.message_type):
50
- raise TypeError(
51
- f"Expected message of type {self.message_type.__name__}, got {type(message).__name__}"
52
- )
53
-
54
- # Publish to all local subscribers
55
- for subscriber in self._subscribers:
56
- try:
57
- subscriber(message)
58
- except Exception as e:
59
- logger.error(
60
- f"Error in subscriber callback for topic {self.topic}: {e}"
61
- )
62
-
63
- # Note: Remote publishing is handled by the Node class, which
64
- # wraps this publish method to publish to the WebSocket transport
65
-
66
- def add_subscriber(self, subscriber: Callable[[Any], None]) -> None:
67
- """Add a subscriber to the publisher"""
68
- if subscriber not in self._subscribers:
69
- self._subscribers.append(subscriber)
70
- logger.debug(f"Added subscriber to topic: {self.topic}")
71
-
72
- def remove_subscriber(self, subscriber: Callable[[Any], None]) -> None:
73
- """Remove a subscriber from the publisher"""
74
- if subscriber in self._subscribers:
75
- self._subscribers.remove(subscriber)
76
- logger.debug(f"Removed subscriber from topic: {self.topic}")
77
-
78
- @classmethod
79
- def get_publishers_for_topic(cls, topic: str) -> List["Publisher"]:
80
- """Get all publishers for a given topic"""
81
- with cls._lock:
82
- return cls._publishers_by_topic.get(topic, [])
83
-
84
- @classmethod
85
- def get_all_topics(cls) -> List[str]:
86
- """Get all topics"""
87
- with cls._lock:
88
- return list(cls._publishers_by_topic.keys())
89
-
90
-
91
- class Subscriber:
92
- # Class dictionary to keep track of all subscribers
93
- _subscribers_by_topic: Dict[str, List["Subscriber"]] = {}
94
- _lock = threading.RLock()
95
-
96
- def __init__(
97
- self,
98
- topic: str,
99
- message_type: Type,
100
- callback: Callable[[Any], None],
101
- queue_size: int = 10,
102
- ):
103
- self.topic = topic
104
- self.message_type = message_type
105
- self.callback = callback
106
- self.queue_size = queue_size
107
-
108
- # Find publishers for this topic and subscribe
109
- self._connect_to_publishers()
110
-
111
- # Register this subscriber in the class dictionary
112
- with Subscriber._lock:
113
- if topic not in Subscriber._subscribers_by_topic:
114
- Subscriber._subscribers_by_topic[topic] = []
115
- Subscriber._subscribers_by_topic[topic].append(self)
116
-
117
- logger.debug(f"Created subscriber for topic: {topic}")
118
-
119
- def __del__(self):
120
- """Delete the subscriber"""
121
- # Unsubscribe from all publishers
122
- publishers = Publisher.get_publishers_for_topic(self.topic)
123
- for publisher in publishers:
124
- publisher.remove_subscriber(self.callback)
125
-
126
- # Remove from class dictionary
127
- with Subscriber._lock:
128
- if self.topic in Subscriber._subscribers_by_topic:
129
- if self in Subscriber._subscribers_by_topic[self.topic]:
130
- Subscriber._subscribers_by_topic[self.topic].remove(self)
131
- if not Subscriber._subscribers_by_topic[self.topic]:
132
- del Subscriber._subscribers_by_topic[self.topic]
133
-
134
- def _connect_to_publishers(self) -> None:
135
- """Connect to all publishers for a given topic"""
136
- publishers = Publisher.get_publishers_for_topic(self.topic)
137
- for publisher in publishers:
138
- if publisher.message_type == self.message_type:
139
- publisher.add_subscriber(self.callback)
140
-
141
- @classmethod
142
- def get_subscribers_for_topic(cls, topic: str) -> List["Subscriber"]:
143
- """Get all subscribers for a given topic"""
144
- with cls._lock:
145
- return cls._subscribers_by_topic.get(topic, [])
@@ -1,81 +0,0 @@
1
- import asyncio
2
- from typing import Generic, Optional, Type, TypeVar
3
-
4
- from .service import (
5
- ServiceNotFoundError,
6
- ServiceServer,
7
- )
8
-
9
- T = TypeVar("T") # Request type
10
- R = TypeVar("R") # Response type
11
-
12
-
13
- class ServiceClient(Generic[T, R]):
14
- def __init__(self, service_type: Type[T], service_name: str):
15
- # ServiceClient attributes
16
- self.service_type = service_type
17
- self.service_name = service_name
18
-
19
- # Initialize the ServiceServer object
20
- self._service: Optional[ServiceServer] = None
21
-
22
- async def connect(self, retry_count: int = 3, retry_delay: float = 1.0) -> bool:
23
- """Connect to the service server"""
24
- for attempt in range(retry_count):
25
- self._service = ServiceServer.get_service(self.service_name)
26
- if self._service is not None:
27
- return True
28
-
29
- if attempt < retry_count - 1:
30
- await asyncio.sleep(retry_delay)
31
-
32
- return False
33
-
34
- async def call(
35
- self, request: T, service_name: str = None, timeout: float = 30.0
36
- ) -> R:
37
- """Call the service server"""
38
- # Default to the client's service name if none provided
39
- if service_name is None:
40
- service_name = self.service_name
41
-
42
- if self.service_name != service_name:
43
- raise ValueError(
44
- f"Service name mismatch. Expected {self.service_name}, got {service_name}"
45
- )
46
-
47
- # Validate request type if possible
48
- request_type = getattr(self.service_type, "Request", self.service_type)
49
- if not isinstance(request, request_type):
50
- raise TypeError(
51
- f"Expected request of type {request_type.__name__}, got {type(request).__name__}"
52
- )
53
-
54
- if self._service is None:
55
- connected = await self.connect()
56
- if not connected:
57
- raise ServiceNotFoundError(f"Service {self.service_name} not found")
58
-
59
- # Check service type compatibility if possible
60
- if (
61
- hasattr(self.service_type, "Request")
62
- and hasattr(self._service.service_type, "Request")
63
- and self.service_type.Request != self._service.service_type.Request
64
- ):
65
- raise TypeError(
66
- f"Service request type mismatch. Expected {self.service_type.Request.__name__}, "
67
- f"got {self._service.service_type.Request.__name__}"
68
- )
69
-
70
- if (
71
- hasattr(self.service_type, "Response")
72
- and hasattr(self._service.service_type, "Response")
73
- and self.service_type.Response != self._service.service_type.Response
74
- ):
75
- raise TypeError(
76
- f"Service response type mismatch. Expected {self.service_type.Response.__name__}, "
77
- f"got {self._service.service_type.Response.__name__}"
78
- )
79
-
80
- # Call the service server
81
- return await self._service.call(request, service_name, timeout=timeout)
@@ -1,135 +0,0 @@
1
- import asyncio
2
- import logging
3
- from typing import Callable, Dict, Generic, List, Optional, Type, TypeVar
4
-
5
- logging.basicConfig(
6
- level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
7
- )
8
- logger = logging.getLogger("service")
9
-
10
- T = TypeVar("T") # Request type
11
- R = TypeVar("R") # Response type
12
-
13
-
14
- class ServiceError(Exception):
15
- """Base exception for service-related errors"""
16
-
17
- pass
18
-
19
-
20
- class ServiceTimeoutError(ServiceError):
21
- """Exception raised when a service call times out"""
22
-
23
- pass
24
-
25
-
26
- class ServiceNotFoundError(ServiceError):
27
- """Exception raised when a service is not found"""
28
-
29
- pass
30
-
31
-
32
- class ServiceHandlerError(ServiceError):
33
- """Exception raised when a service handler raises an exception"""
34
-
35
- pass
36
-
37
-
38
- class ServiceServer(Generic[T, R]):
39
- """Enhanced service implementation with timeout and error handling"""
40
-
41
- # Class dictionary to keep track of all services
42
- _services: Dict[str, "ServiceServer"] = {}
43
- _lock = asyncio.Lock()
44
-
45
- def __init__(
46
- self, service_type: Type[T], service_name: str, handler: Callable[[T], R]
47
- ):
48
- self._handler: Optional[Callable[[T], R]] = None
49
-
50
- self.service_type = service_type
51
- self.service_name = service_name
52
-
53
- self.set_handler(handler)
54
-
55
- # Register this service in the class dictionary
56
- ServiceServer._services[service_name] = self
57
-
58
- logger.debug(f"Created service: {service_name}")
59
-
60
- def set_handler(self, handler: Callable[[T], R]) -> None:
61
- """Set the handler for the service"""
62
- self._handler = handler
63
- logger.debug(f"Set handler for service: {self.service_name}")
64
-
65
- async def call(self, request: T, service_name: str, timeout: float = 30.0) -> R:
66
- """Call the service"""
67
- if self.service_name != service_name:
68
- raise ValueError(
69
- f"Service name mismatch. Expected {self.service_name}, got {service_name}"
70
- )
71
-
72
- # Validate the request type if possible
73
- request_type = getattr(self.service_type, "Request")
74
- if not isinstance(request, request_type):
75
- raise TypeError(
76
- f"Expected request of type {request_type.__name__}, got {type(request).__name__}"
77
- )
78
-
79
- if self._handler is None:
80
- raise ServiceNotFoundError(
81
- f"No handler set for service {self.service_name}"
82
- )
83
-
84
- try:
85
- # Run the handler with timeout
86
- return await asyncio.wait_for(self._run_handler(request), timeout=timeout)
87
- except asyncio.TimeoutError:
88
- raise ServiceTimeoutError(
89
- f"Service call to {self.service_name} timed out after {timeout} seconds"
90
- )
91
-
92
- async def _run_handler(self, request: T) -> R:
93
- """Run the server handler and handle exceptions"""
94
- try:
95
- # If handler is already a coroutine function, await it
96
- if asyncio.iscoroutinefunction(self._handler):
97
- result = await self._handler(request)
98
- else:
99
- # Run non-async handler in a thread pool
100
- loop = asyncio.get_event_loop()
101
- result = await loop.run_in_executor(None, self._handler, request)
102
-
103
- # Type check the result if possible
104
- response_type = getattr(self.service_type, "Response", None)
105
- if response_type and not isinstance(result, response_type):
106
- raise TypeError(
107
- f"Service {self.service_name} returned {type(result).__name__}, expected {response_type.__name__}"
108
- )
109
-
110
- return result
111
- except Exception as e:
112
- logger.error(f"Error in service handler for {self.service_name}: {e}")
113
- raise ServiceHandlerError(
114
- f"Service handler for {self.service_name} raised an exception: {e}"
115
- )
116
-
117
- @classmethod
118
- def get_service(cls, service_name: str) -> Optional["ServiceServer"]:
119
- """Get a service by name"""
120
- return cls._services.get(service_name)
121
-
122
- @classmethod
123
- def get_all_services(cls) -> List[str]:
124
- """Get all services"""
125
- return list(cls._services.keys())
126
-
127
-
128
- def get_service(name: str) -> Optional[ServiceServer]:
129
- """External helper function to get a service by name"""
130
- return ServiceServer.get_service(name)
131
-
132
-
133
- def get_all_services() -> List[str]:
134
- """External helper function to get all services"""
135
- return ServiceServer.get_all_services()
@@ -1,83 +0,0 @@
1
- """Data models for messaging system.
2
-
3
- This module defines the data models used for request and response messaging.
4
- """
5
-
6
- from typing import Any, Dict, Optional
7
- from ...core.models import ObservationModel
8
-
9
- from pydantic import BaseModel, Field
10
-
11
-
12
- class ServiceRequest(BaseModel):
13
- """Base class for service requests"""
14
-
15
- pass
16
-
17
-
18
- class ServiceResponse(BaseModel):
19
- """Base class for service responses"""
20
-
21
- success: bool = Field(
22
- default=True, description="Whether the service call was successful"
23
- )
24
- message: str = Field(
25
- default="", description="A message describing the service call"
26
- )
27
-
28
- request_type: str = Field(
29
- description="Type of response (reset_response or step_response)"
30
- )
31
- request_id: str = Field(description="Unique identifier")
32
- time_stamp: str = Field(alias="timeStamp", description="Timestamp value")
33
-
34
- observation: ObservationModel = Field(description="Observation data")
35
- info: Dict[str, str] = Field(description="Additional information")
36
-
37
- class Config:
38
- populate_by_name = True
39
-
40
-
41
- class ResetRequest(ServiceRequest):
42
- """Request to reset the robot"""
43
-
44
- seed: Optional[int] = Field(
45
- default=None, description="The seed to reset the robot with"
46
- )
47
- options: Optional[Dict[str, Any]] = Field(
48
- default=None, description="Options for the reset"
49
- )
50
-
51
-
52
- class ResetResponse(ServiceResponse):
53
- """Response to reset request"""
54
-
55
- pass
56
-
57
-
58
- class Reset:
59
- """Reset the robot"""
60
-
61
- Request = ResetRequest
62
- Response = ResetResponse
63
-
64
-
65
- class StepRequest(ServiceRequest):
66
- """Request to step the robot with an action"""
67
-
68
- actuator_values: list = Field(
69
- description="The array of actuator values to control the robot with"
70
- )
71
-
72
-
73
- class StepResponse(ServiceResponse):
74
- """Response to step request"""
75
-
76
- pass
77
-
78
-
79
- class Step:
80
- """Step the robot with an action"""
81
-
82
- Request = StepRequest
83
- Response = StepResponse