neuracore 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Neuraco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.1
2
+ Name: neuracore
3
+ Version: 1.0.0
4
+ Summary: Neuracore Client Library
5
+ Home-page: https://github.com/neuraco/neuracore
6
+ Author: Stephen James
7
+ Author-email: stephen@neuraco.com
8
+ Keywords: robotics machine-learning ai client-library
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Provides-Extra: examples
19
+ Provides-Extra: local_endpoint
20
+ Provides-Extra: dev
21
+ License-File: LICENSE
22
+
23
+ # Neuracore Python Client
24
+
25
+ Neuracore is a powerful robotics and machine learning client library for seamless robot data collection, model deployment, and interaction.
26
+
27
+ ## Features
28
+
29
+ - Easy robot initialization and connection
30
+ - Streaming data logging
31
+ - Model endpoint management
32
+ - Local and remote model support
33
+ - Flexible dataset creation
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install neuracore
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ Ensure you have an account at (https://www.neuracore.app/)[https://www.neuracore.app/]
44
+
45
+ ### Authentication
46
+
47
+ ```python
48
+ import neuracore as nc
49
+
50
+ # This will save your API key locally
51
+ nc.login()
52
+ ```
53
+
54
+ ### Robot Connection
55
+
56
+ ```python
57
+ # Connect to a robot
58
+ nc.connect_robot(
59
+ robot_name="MyRobot",
60
+ urdf_path="/path/to/robot.urdf"
61
+ )
62
+ ```
63
+
64
+ ### Data Logging
65
+
66
+ ```python
67
+ # Log joint positions
68
+ nc.log_joints({
69
+ 'joint1': 0.5,
70
+ 'joint2': -0.3
71
+ })
72
+
73
+ # Log RGB camera image
74
+ nc.log_rgb("top_camera", image_array)
75
+ ```
76
+
77
+ ## Development
78
+
79
+ To set up for development:
80
+
81
+ ```bash
82
+ git clone https://github.com/neuraco/neuracore
83
+ cd neuracore
84
+ pip install -e .[dev]
85
+ ```
86
+
87
+ ## Testing
88
+
89
+ ```bash
90
+ export NEURACORE_API_URL=http://localhost:8000/api
91
+ pytest tests/
92
+ ```
93
+
94
+ ## Contributing
95
+
96
+ Contributions are welcome!
@@ -0,0 +1,74 @@
1
+ # Neuracore Python Client
2
+
3
+ Neuracore is a powerful robotics and machine learning client library for seamless robot data collection, model deployment, and interaction.
4
+
5
+ ## Features
6
+
7
+ - Easy robot initialization and connection
8
+ - Streaming data logging
9
+ - Model endpoint management
10
+ - Local and remote model support
11
+ - Flexible dataset creation
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install neuracore
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ Ensure you have an account at (https://www.neuracore.app/)[https://www.neuracore.app/]
22
+
23
+ ### Authentication
24
+
25
+ ```python
26
+ import neuracore as nc
27
+
28
+ # This will save your API key locally
29
+ nc.login()
30
+ ```
31
+
32
+ ### Robot Connection
33
+
34
+ ```python
35
+ # Connect to a robot
36
+ nc.connect_robot(
37
+ robot_name="MyRobot",
38
+ urdf_path="/path/to/robot.urdf"
39
+ )
40
+ ```
41
+
42
+ ### Data Logging
43
+
44
+ ```python
45
+ # Log joint positions
46
+ nc.log_joints({
47
+ 'joint1': 0.5,
48
+ 'joint2': -0.3
49
+ })
50
+
51
+ # Log RGB camera image
52
+ nc.log_rgb("top_camera", image_array)
53
+ ```
54
+
55
+ ## Development
56
+
57
+ To set up for development:
58
+
59
+ ```bash
60
+ git clone https://github.com/neuraco/neuracore
61
+ cd neuracore
62
+ pip install -e .[dev]
63
+ ```
64
+
65
+ ## Testing
66
+
67
+ ```bash
68
+ export NEURACORE_API_URL=http://localhost:8000/api
69
+ pytest tests/
70
+ ```
71
+
72
+ ## Contributing
73
+
74
+ Contributions are welcome!
@@ -0,0 +1,3 @@
1
+ from .core import * # noqa: F403
2
+
3
+ __version__ = "1.0.0"
@@ -0,0 +1,125 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+
5
+ import requests
6
+
7
+ from neuracore.const import API_URL
8
+
9
+ from .exceptions import AuthenticationError
10
+ from .generate_api_key import generate_api_key
11
+
12
+ CONFIG_DIR = Path.home() / ".neuracore"
13
+ CONFIG_FILE = "config.json"
14
+
15
+
16
+ class Auth:
17
+ _instance = None
18
+ _api_key: str | None = None
19
+
20
+ def __new__(cls):
21
+ if cls._instance is None:
22
+ cls._instance = super().__new__(cls)
23
+ return cls._instance
24
+
25
+ def __init__(self):
26
+ self._load_config()
27
+ self._access_token = None
28
+
29
+ def _load_config(self) -> None:
30
+ """Load configuration from disk if it exists."""
31
+ config_file = CONFIG_DIR / CONFIG_FILE
32
+ if config_file.exists():
33
+ with open(config_file) as f:
34
+ config = json.load(f)
35
+ self._api_key = config.get("api_key")
36
+
37
+ def _save_config(self) -> None:
38
+ """Save current configuration to disk."""
39
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
40
+ config_file = CONFIG_DIR / CONFIG_FILE
41
+ with open(config_file, "w") as f:
42
+ json.dump({"api_key": self._api_key}, f)
43
+
44
+ def login(self, api_key: str | None = None) -> None:
45
+ """
46
+ Authenticate with the NeuraCore server using an API key.
47
+
48
+ Args:
49
+ api_key: Optional API key. If not provided, will try to use environment
50
+ variable NEURACORE_API_KEY or previously saved config.
51
+ """
52
+ self._api_key = api_key or os.environ.get("NEURACORE_API_KEY") or self._api_key
53
+
54
+ if not self._api_key:
55
+ print("No API key provided. Attempting to log you in...")
56
+ self._api_key = generate_api_key()
57
+
58
+ # Verify API key with server and get access token
59
+ try:
60
+ response = requests.post(
61
+ f"{API_URL}/auth/verify-api-key",
62
+ json={"api_key": self._api_key},
63
+ )
64
+ if response.status_code != 200:
65
+ raise AuthenticationError(
66
+ f"Failed to authenticate: {response.json().get('detail')}"
67
+ )
68
+ token_data = response.json()
69
+ self._access_token = token_data["access_token"]
70
+ except requests.exceptions.RequestException as e:
71
+ raise AuthenticationError(f"Failed to authenticate: {str(e)}")
72
+
73
+ # Save configuration if verification successful
74
+ self._save_config()
75
+
76
+ def logout(self) -> None:
77
+ """Clear authentication state."""
78
+ self._api_key = None
79
+ self._access_token = None
80
+ config_file = CONFIG_DIR / CONFIG_FILE
81
+ if config_file.exists():
82
+ config_file.unlink()
83
+
84
+ @property
85
+ def api_key(self) -> str | None:
86
+ """Get the current API key."""
87
+ return self._api_key
88
+
89
+ @property
90
+ def access_token(self) -> str | None:
91
+ """Get the current access token."""
92
+ return self._access_token
93
+
94
+ @property
95
+ def is_authenticated(self) -> bool:
96
+ """Check if currently authenticated."""
97
+ return self._api_key is not None and self._access_token is not None
98
+
99
+ def get_headers(self) -> dict:
100
+ """Get headers for authenticated requests."""
101
+ if not self.is_authenticated:
102
+ raise AuthenticationError("Not authenticated. Please call login() first.")
103
+ return {
104
+ "Authorization": f"Bearer {self._access_token}",
105
+ # "Content-Type": "application/json",
106
+ }
107
+
108
+
109
+ # Global instance
110
+ _auth = Auth()
111
+
112
+
113
+ def login(api_key: str | None = None) -> None:
114
+ """Global login function."""
115
+ _auth.login(api_key)
116
+
117
+
118
+ def logout() -> None:
119
+ """Global logout function."""
120
+ _auth.logout()
121
+
122
+
123
+ def get_auth() -> Auth:
124
+ """Get the global Auth instance."""
125
+ return _auth
@@ -0,0 +1,3 @@
1
+ import os
2
+
3
+ API_URL = os.getenv("NEURACORE_API_URL", "https://api.neuracore.app/api")
@@ -0,0 +1,250 @@
1
+ import atexit
2
+
3
+ import numpy as np
4
+
5
+ from .auth import login as _login
6
+ from .auth import logout as _logout
7
+ from .dataset import Dataset
8
+ from .endpoint import EndpointPolicy
9
+ from .endpoint import connect_endpoint as _connect_endpoint
10
+ from .endpoint import connect_local_endpoint as _connect_local_endpoint
11
+ from .exceptions import RobotError
12
+ from .robot import Robot, get_robot
13
+ from .robot import init as _init_robot
14
+ from .streaming import log_action as _log_action
15
+ from .streaming import log_depth as _log_depth
16
+ from .streaming import log_joints as _log_joints
17
+ from .streaming import log_rgb as _log_rgb
18
+ from .streaming import stop_all_streams as _stop_all_streams
19
+ from .streaming import stop_streaming as _stop_streaming
20
+ from .streaming import wait_until_stream_empty
21
+
22
+ # Global active robot ID - allows us to avoid passing robot_name to every call
23
+ _active_robot: Robot | None = None
24
+ _active_dataset_id: str | None = None
25
+ _active_recording_id: str | None = None
26
+
27
+
28
+ def login(api_key: str | None = None) -> None:
29
+ """
30
+ Authenticate with NeuraCore server.
31
+
32
+ Args:
33
+ api_key: Optional API key. If not provided, will look for NEURACORE_API_KEY
34
+ environment variable or previously saved configuration.
35
+
36
+ Raises:
37
+ AuthenticationError: If authentication fails
38
+ """
39
+ _login(api_key)
40
+
41
+
42
+ def logout() -> None:
43
+ """Clear authentication state."""
44
+ _logout()
45
+
46
+
47
+ def connect_robot(
48
+ robot_name: str, urdf_path: str | None = None, overwrite: bool = False
49
+ ) -> None:
50
+ """
51
+ Initialize a robot connection.
52
+
53
+ Args:
54
+ robot_name: Unique identifier for the robot
55
+ urdf_path: Optional path to robot's URDF file
56
+ """
57
+ global _active_robot
58
+ _active_robot = _init_robot(robot_name, urdf_path, overwrite)
59
+
60
+
61
+ def _get_robot(robot_name: str) -> Robot:
62
+ """Get a robot by name."""
63
+ robot: Robot = _active_robot
64
+ if robot_name is None:
65
+ if _active_robot is None:
66
+ raise RobotError(
67
+ "No active robot. Call init() first or provide robot_name."
68
+ )
69
+ else:
70
+ robot = get_robot(robot_name)
71
+ return robot
72
+
73
+
74
+ def log_joints(positions: dict[str, float], robot_name: str | None = None) -> None:
75
+ """
76
+ Log joint positions for a robot.
77
+
78
+ Args:
79
+ positions: Dictionary mapping joint names to positions (in radians)
80
+ robot_name: Optional robot ID. If not provided, uses the last initialized robot
81
+
82
+ Raises:
83
+ RobotError: If no robot is active and no robot_name provided
84
+ StreamingError: If logging fails
85
+ """
86
+ _log_joints(_get_robot(robot_name), positions)
87
+
88
+
89
+ def log_action(action: dict[str, float], robot_name: str | None = None) -> None:
90
+ """
91
+ Log action for a robot.
92
+
93
+ Args:
94
+ action: Dictionary mapping joint names to positions (in radians)
95
+ robot_name: Optional robot ID. If not provided, uses the last initialized robot
96
+
97
+ Raises:
98
+ RobotError: If no robot is active and no robot_name provided
99
+ StreamingError: If logging fails
100
+ """
101
+ _log_action(_get_robot(robot_name), action)
102
+
103
+
104
+ def log_rgb(camera_id: str, image: np.ndarray, robot_name: str | None = None) -> None:
105
+ """
106
+ Log RGB image from a camera.
107
+
108
+ Args:
109
+ camera_id: Unique identifier for the camera
110
+ image: RGB image as numpy array (HxWx3, dtype=uint8 or float32)
111
+ robot_name: Optional robot ID. If not provided, uses the last initialized robot
112
+
113
+ Raises:
114
+ RobotError: If no robot is active and no robot_name provided
115
+ StreamingError: If logging fails
116
+ ValueError: If image format is invalid
117
+ """
118
+ _log_rgb(_get_robot(robot_name), camera_id, image)
119
+
120
+
121
+ def log_depth(camera_id: str, depth: np.ndarray, robot_name: str | None = None) -> None:
122
+ """
123
+ Log depth image from a camera.
124
+
125
+ Args:
126
+ camera_id: Unique identifier for the camera
127
+ depth: Depth image as numpy array (HxW, dtype=float32, in meters)
128
+ robot_name: Optional robot ID. If not provided, uses the last initialized robot
129
+
130
+ Raises:
131
+ RobotError: If no robot is active and no robot_name provided
132
+ StreamingError: If logging fails
133
+ ValueError: If depth format is invalid
134
+ """
135
+ _log_depth(_get_robot(robot_name), camera_id, depth)
136
+
137
+
138
+ def start_recording(robot_name: str | None = None) -> None:
139
+ """
140
+ Start recording data for a specific robot.
141
+
142
+ Args:
143
+ robot_name: Optional robot ID. If not provided, uses the last initialized robot
144
+
145
+ Raises:
146
+ RobotError: If no robot is active and no robot_name provided
147
+ """
148
+ global _active_recording_id
149
+ if _active_recording_id is not None:
150
+ raise RobotError("Recording already in progress. Call stop_recording() first.")
151
+ robot = _get_robot(robot_name)
152
+ if _active_dataset_id is None:
153
+ raise RobotError("No active dataset. Call create_dataset() first.")
154
+ _active_recording_id = robot.start_recording(_active_dataset_id)
155
+
156
+
157
+ def stop_recording(robot_name: str | None = None) -> None:
158
+ """
159
+ Stop recording data for a specific robot.
160
+
161
+ Args:
162
+ robot_name: Optional robot ID. If not provided, uses the last initialized robot
163
+
164
+ Raises:
165
+ RobotError: If no robot is active and no robot_name provided
166
+ """
167
+ global _active_recording_id
168
+ robot = _get_robot(robot_name)
169
+ if _active_recording_id is None:
170
+ raise RobotError("No active recording. Call start_recording() first.")
171
+ wait_until_stream_empty(robot)
172
+ robot.stop_recording(_active_recording_id)
173
+ _active_recording_id = None
174
+
175
+
176
+ def create_dataset(
177
+ name: str, description: str | None = None, tags: list[str] | None = None
178
+ ) -> None:
179
+ """
180
+ Create a new dataset for robot demonstrations.
181
+
182
+ Args:
183
+ name: Dataset name
184
+ description: Optional description
185
+ tags: Optional list of tags
186
+
187
+ Raises:
188
+ DatasetError: If dataset creation fails
189
+ """
190
+ global _active_dataset_id
191
+ _active_dataset = Dataset(name, description, tags)
192
+ _active_dataset_id = _active_dataset.id
193
+
194
+
195
+ def connect_endpoint(name: str) -> EndpointPolicy:
196
+ """
197
+ Connect to a deployed model endpoint.
198
+
199
+ Args:
200
+ name: Name of the deployed endpoint
201
+
202
+ Returns:
203
+ EndpointPolicy: Policy object that can be used for predictions
204
+
205
+ Raises:
206
+ EndpointError: If endpoint connection fails
207
+ """
208
+ return _connect_endpoint(name)
209
+
210
+
211
+ def connect_local_endpoint(path_to_model: str) -> EndpointPolicy:
212
+ """
213
+ Connect to a local model endpoint.
214
+
215
+ Args:
216
+ path_to_model: Path to the local model endpoint
217
+
218
+ Returns:
219
+ EndpointPolicy: Policy object that can be used for predictions
220
+
221
+ Raises:
222
+ EndpointError: If endpoint connection fails
223
+ """
224
+ return _connect_local_endpoint(path_to_model)
225
+
226
+
227
+ def stop(robot_name: str | None = None) -> None:
228
+ """
229
+ Stop streaming for a specific robot.
230
+
231
+ Args:
232
+ robot_name: Optional robot ID. If not provided, uses the last initialized robot
233
+
234
+ Raises:
235
+ RobotError: If no robot is active and no robot_name provided
236
+ """
237
+ global _active_robot
238
+ _stop_streaming(_get_robot(robot_name))
239
+ if robot_name == _active_robot.name:
240
+ _active_robot = None
241
+
242
+
243
+ def stop_all() -> None:
244
+ """Stop all active data streams."""
245
+ global _active_robot
246
+ _stop_all_streams()
247
+ _active_robot = None
248
+
249
+
250
+ atexit.register(stop_all)
@@ -0,0 +1,46 @@
1
+ import logging
2
+
3
+ import requests
4
+
5
+ from neuracore.const import API_URL
6
+
7
+ from .auth import Auth, get_auth
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class Dataset:
13
+
14
+ def __init__(
15
+ self, name: str, description: str | None = None, tags: list[str] | None = None
16
+ ):
17
+ datasets = self._get_datasets()
18
+ for dataset in datasets:
19
+ if dataset["name"] == name:
20
+ self.id = dataset["id"]
21
+ logger.info(f"Dataset '{name}' already exist.")
22
+ return
23
+ id = self._create_dataset(name, description, tags)
24
+ self.id = id
25
+
26
+ def _create_dataset(
27
+ self, name: str, description: str | None = None, tags: list[str] | None = None
28
+ ):
29
+ auth: Auth = get_auth()
30
+ response = requests.post(
31
+ f"{API_URL}/api/datasets",
32
+ headers=auth.get_headers(),
33
+ json={
34
+ "name": name,
35
+ "description": description,
36
+ },
37
+ )
38
+ response.raise_for_status()
39
+ dataset_json = response.json()
40
+ return dataset_json["id"]
41
+
42
+ def _get_datasets(self):
43
+ auth: Auth = get_auth()
44
+ response = requests.get(f"{API_URL}/api/datasets", headers=auth.get_headers())
45
+ response.raise_for_status()
46
+ return response.json()