luckyrobots 0.1.72__py3-none-any.whl → 0.1.73__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.
- luckyrobots/__init__.py +5 -32
- luckyrobots/client.py +20 -491
- luckyrobots/config/robots.yaml +72 -48
- luckyrobots/engine/__init__.py +5 -20
- luckyrobots/models/__init__.py +2 -14
- luckyrobots/models/observation.py +4 -33
- luckyrobots/utils.py +1 -43
- {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.73.dist-info}/METADATA +1 -1
- {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.73.dist-info}/RECORD +11 -15
- luckyrobots/engine/check_updates.py +0 -264
- luckyrobots/engine/download.py +0 -125
- luckyrobots/models/camera.py +0 -97
- luckyrobots/models/randomization.py +0 -77
- {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.73.dist-info}/WHEEL +0 -0
- {luckyrobots-0.1.72.dist-info → luckyrobots-0.1.73.dist-info}/licenses/LICENSE +0 -0
luckyrobots/config/robots.yaml
CHANGED
|
@@ -149,103 +149,127 @@ unitreego1:
|
|
|
149
149
|
- locomotion
|
|
150
150
|
action_space:
|
|
151
151
|
actuator_names:
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
-
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
152
|
+
- FR_hip_joint
|
|
153
|
+
- FR_thigh_joint
|
|
154
|
+
- FR_calf_joint
|
|
155
|
+
- FL_hip_joint
|
|
156
|
+
- FL_thigh_joint
|
|
157
|
+
- FL_calf_joint
|
|
158
|
+
- RR_hip_joint
|
|
159
|
+
- RR_thigh_joint
|
|
160
|
+
- RR_calf_joint
|
|
161
|
+
- RL_hip_joint
|
|
162
|
+
- RL_thigh_joint
|
|
163
|
+
- RL_calf_joint
|
|
164
164
|
actuator_limits:
|
|
165
|
-
- name:
|
|
165
|
+
- name: FR_hip_joint
|
|
166
166
|
lower: -0.863
|
|
167
167
|
upper: 0.863
|
|
168
|
-
|
|
168
|
+
default: 0.1
|
|
169
|
+
- name: FR_thigh_joint
|
|
169
170
|
lower: -0.686
|
|
170
171
|
upper: 4.501
|
|
171
|
-
|
|
172
|
+
default: 0.9
|
|
173
|
+
- name: FR_calf_joint
|
|
172
174
|
lower: -2.818
|
|
173
175
|
upper: -0.888
|
|
174
|
-
|
|
176
|
+
default: -1.8
|
|
177
|
+
- name: FL_hip_joint
|
|
175
178
|
lower: -0.863
|
|
176
179
|
upper: 0.863
|
|
177
|
-
|
|
180
|
+
default: -0.1
|
|
181
|
+
- name: FL_thigh_joint
|
|
178
182
|
lower: -0.686
|
|
179
183
|
upper: 4.501
|
|
180
|
-
|
|
184
|
+
default: 0.9
|
|
185
|
+
- name: FL_calf_joint
|
|
181
186
|
lower: -2.818
|
|
182
187
|
upper: -0.888
|
|
183
|
-
|
|
188
|
+
default: -1.8
|
|
189
|
+
- name: RR_hip_joint
|
|
184
190
|
lower: -0.863
|
|
185
191
|
upper: 0.863
|
|
186
|
-
|
|
192
|
+
default: 0.1
|
|
193
|
+
- name: RR_thigh_joint
|
|
187
194
|
lower: -0.686
|
|
188
195
|
upper: 4.501
|
|
189
|
-
|
|
196
|
+
default: 0.9
|
|
197
|
+
- name: RR_calf_joint
|
|
190
198
|
lower: -2.818
|
|
191
199
|
upper: -0.888
|
|
192
|
-
|
|
200
|
+
default: -1.8
|
|
201
|
+
- name: RL_hip_joint
|
|
193
202
|
lower: -0.863
|
|
194
203
|
upper: 0.863
|
|
195
|
-
|
|
204
|
+
default: -0.1
|
|
205
|
+
- name: RL_thigh_joint
|
|
196
206
|
lower: -0.686
|
|
197
207
|
upper: 4.501
|
|
198
|
-
|
|
208
|
+
default: 0.9
|
|
209
|
+
- name: RL_calf_joint
|
|
199
210
|
lower: -2.818
|
|
200
211
|
upper: -0.888
|
|
212
|
+
default: -1.8
|
|
201
213
|
observation_space:
|
|
202
214
|
actuator_names:
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
-
|
|
211
|
-
-
|
|
212
|
-
-
|
|
213
|
-
-
|
|
214
|
-
-
|
|
215
|
+
- FR_hip_joint
|
|
216
|
+
- FR_thigh_joint
|
|
217
|
+
- FR_calf_joint
|
|
218
|
+
- FL_hip_joint
|
|
219
|
+
- FL_thigh_joint
|
|
220
|
+
- FL_calf_joint
|
|
221
|
+
- RR_hip_joint
|
|
222
|
+
- RR_thigh_joint
|
|
223
|
+
- RR_calf_joint
|
|
224
|
+
- RL_hip_joint
|
|
225
|
+
- RL_thigh_joint
|
|
226
|
+
- RL_calf_joint
|
|
215
227
|
actuator_limits:
|
|
216
|
-
- name:
|
|
228
|
+
- name: FR_hip_joint
|
|
217
229
|
lower: -0.863
|
|
218
230
|
upper: 0.863
|
|
219
|
-
|
|
231
|
+
default: 0.1
|
|
232
|
+
- name: FR_thigh_joint
|
|
220
233
|
lower: -0.686
|
|
221
234
|
upper: 4.501
|
|
222
|
-
|
|
235
|
+
default: 0.9
|
|
236
|
+
- name: FR_calf_joint
|
|
223
237
|
lower: -2.818
|
|
224
238
|
upper: -0.888
|
|
225
|
-
|
|
239
|
+
default: -1.8
|
|
240
|
+
- name: FL_hip_joint
|
|
226
241
|
lower: -0.863
|
|
227
242
|
upper: 0.863
|
|
228
|
-
|
|
243
|
+
default: -0.1
|
|
244
|
+
- name: FL_thigh_joint
|
|
229
245
|
lower: -0.686
|
|
230
246
|
upper: 4.501
|
|
231
|
-
|
|
247
|
+
default: 0.9
|
|
248
|
+
- name: FL_calf_joint
|
|
232
249
|
lower: -2.818
|
|
233
250
|
upper: -0.888
|
|
234
|
-
|
|
251
|
+
default: -1.8
|
|
252
|
+
- name: RR_hip_joint
|
|
235
253
|
lower: -0.863
|
|
236
254
|
upper: 0.863
|
|
237
|
-
|
|
255
|
+
default: 0.1
|
|
256
|
+
- name: RR_thigh_joint
|
|
238
257
|
lower: -0.686
|
|
239
258
|
upper: 4.501
|
|
240
|
-
|
|
259
|
+
default: 0.9
|
|
260
|
+
- name: RR_calf_joint
|
|
241
261
|
lower: -2.818
|
|
242
262
|
upper: -0.888
|
|
243
|
-
|
|
263
|
+
default: -1.8
|
|
264
|
+
- name: RL_hip_joint
|
|
244
265
|
lower: -0.863
|
|
245
266
|
upper: 0.863
|
|
246
|
-
|
|
267
|
+
default: -0.1
|
|
268
|
+
- name: RL_thigh_joint
|
|
247
269
|
lower: -0.686
|
|
248
270
|
upper: 4.501
|
|
249
|
-
|
|
271
|
+
default: 0.9
|
|
272
|
+
- name: RL_calf_joint
|
|
250
273
|
lower: -2.818
|
|
251
274
|
upper: -0.888
|
|
275
|
+
default: -1.8
|
luckyrobots/engine/__init__.py
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
1
|
"""Engine lifecycle management for LuckyEngine."""
|
|
2
2
|
|
|
3
|
-
from .
|
|
4
|
-
|
|
5
|
-
from .manager import (
|
|
6
|
-
find_luckyengine_executable,
|
|
7
|
-
is_luckyengine_running,
|
|
8
|
-
launch_luckyengine,
|
|
9
|
-
stop_luckyengine,
|
|
3
|
+
from luckyrobots.engine.manager import (
|
|
4
|
+
find_luckyengine_executable as find_luckyengine_executable,
|
|
10
5
|
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"launch_luckyengine",
|
|
15
|
-
"stop_luckyengine",
|
|
16
|
-
"is_luckyengine_running",
|
|
17
|
-
"find_luckyengine_executable",
|
|
18
|
-
# Update functions
|
|
19
|
-
"check_updates",
|
|
20
|
-
"apply_changes",
|
|
21
|
-
"get_base_url",
|
|
22
|
-
"get_os_type",
|
|
23
|
-
]
|
|
6
|
+
from luckyrobots.engine.manager import is_luckyengine_running as is_luckyengine_running
|
|
7
|
+
from luckyrobots.engine.manager import launch_luckyengine as launch_luckyengine
|
|
8
|
+
from luckyrobots.engine.manager import stop_luckyengine as stop_luckyengine
|
luckyrobots/models/__init__.py
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Pydantic models for LuckyRobots.
|
|
3
|
-
"""
|
|
1
|
+
"""Pydantic models for LuckyRobots."""
|
|
4
2
|
|
|
5
|
-
from .observation import ObservationResponse
|
|
6
|
-
from .camera import CameraData, CameraShape
|
|
7
|
-
from .randomization import DomainRandomizationConfig
|
|
8
|
-
|
|
9
|
-
__all__ = [
|
|
10
|
-
"ObservationResponse",
|
|
11
|
-
"StateSnapshot",
|
|
12
|
-
"CameraData",
|
|
13
|
-
"CameraShape",
|
|
14
|
-
"DomainRandomizationConfig",
|
|
15
|
-
]
|
|
3
|
+
from luckyrobots.models.observation import ObservationResponse as ObservationResponse
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
"""
|
|
2
|
-
RL observation models for LuckyRobots.
|
|
1
|
+
"""RL observation models for LuckyRobots."""
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
- ObservationResponse: returned by get_observation()
|
|
6
|
-
- StateSnapshot: returned by get_state()
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from typing import Any, Dict, List, Optional
|
|
3
|
+
from typing import Dict, List, Optional
|
|
10
4
|
from pydantic import BaseModel, Field, ConfigDict
|
|
11
5
|
|
|
12
6
|
|
|
@@ -14,11 +8,11 @@ class ObservationResponse(BaseModel):
|
|
|
14
8
|
"""RL observation data from an agent.
|
|
15
9
|
|
|
16
10
|
This is the return type for LuckyEngineClient.get_observation() and
|
|
17
|
-
|
|
11
|
+
LuckyEngineClient.step(). It contains the RL observation vector
|
|
18
12
|
with optional named access for debugging.
|
|
19
13
|
|
|
20
14
|
Usage:
|
|
21
|
-
obs = client.
|
|
15
|
+
obs = client.step(actions)
|
|
22
16
|
|
|
23
17
|
# Flat vector for RL training
|
|
24
18
|
obs.observation # [0.1, 0.2, 0.3, ...]
|
|
@@ -110,26 +104,3 @@ class ObservationResponse(BaseModel):
|
|
|
110
104
|
if self.action_names is not None:
|
|
111
105
|
return dict(zip(self.action_names, self.actions))
|
|
112
106
|
return {f"action_{i}": v for i, v in enumerate(self.actions)}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class StateSnapshot(BaseModel):
|
|
116
|
-
"""Bundled snapshot of multiple data sources.
|
|
117
|
-
|
|
118
|
-
Use LuckyEngineClient.get_state() to get a bundled snapshot when you need
|
|
119
|
-
multiple data types in one efficient call. For streaming data like telemetry,
|
|
120
|
-
use the dedicated streaming methods instead.
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
|
124
|
-
|
|
125
|
-
observation: Optional[ObservationResponse] = Field(
|
|
126
|
-
default=None, description="RL observation data (if include_observation=True)"
|
|
127
|
-
)
|
|
128
|
-
joint_state: Optional[Any] = Field(
|
|
129
|
-
default=None, description="Joint positions/velocities (if include_joint_state=True)"
|
|
130
|
-
)
|
|
131
|
-
camera_frames: Optional[List[Any]] = Field(
|
|
132
|
-
default=None, description="Camera frames (if camera_names provided)"
|
|
133
|
-
)
|
|
134
|
-
timestamp_ms: int = Field(default=0, description="Wall-clock timestamp in milliseconds")
|
|
135
|
-
frame_number: int = Field(default=0, description="Monotonic frame counter")
|
luckyrobots/utils.py
CHANGED
|
@@ -1,49 +1,7 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Utility functions and classes for LuckyRobots.
|
|
3
|
-
"""
|
|
1
|
+
"""Utility functions for LuckyRobots."""
|
|
4
2
|
|
|
5
|
-
import time
|
|
6
3
|
import yaml
|
|
7
4
|
import importlib.resources
|
|
8
|
-
from collections import deque
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class FPS:
|
|
12
|
-
"""Utility for measuring frames per second with a rolling window.
|
|
13
|
-
|
|
14
|
-
Usage:
|
|
15
|
-
fps = FPS(frame_window=30)
|
|
16
|
-
while running:
|
|
17
|
-
# ... do work ...
|
|
18
|
-
current_fps = fps.measure()
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __init__(self, frame_window: int = 30):
|
|
22
|
-
"""Initialize FPS counter.
|
|
23
|
-
|
|
24
|
-
Args:
|
|
25
|
-
frame_window: Number of frames to average over.
|
|
26
|
-
"""
|
|
27
|
-
self.frame_window = frame_window
|
|
28
|
-
self.frame_times: deque[float] = deque(maxlen=frame_window)
|
|
29
|
-
self.last_frame_time = time.perf_counter()
|
|
30
|
-
|
|
31
|
-
def measure(self) -> float:
|
|
32
|
-
"""Record a frame and return current FPS.
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
Current frames per second (averaged over window).
|
|
36
|
-
"""
|
|
37
|
-
current_time = time.perf_counter()
|
|
38
|
-
frame_delta = current_time - self.last_frame_time
|
|
39
|
-
self.last_frame_time = current_time
|
|
40
|
-
|
|
41
|
-
self.frame_times.append(frame_delta)
|
|
42
|
-
|
|
43
|
-
if len(self.frame_times) >= 2:
|
|
44
|
-
avg_frame_time = sum(self.frame_times) / len(self.frame_times)
|
|
45
|
-
return 1.0 / avg_frame_time if avg_frame_time > 0 else 0.0
|
|
46
|
-
return 0.0
|
|
47
5
|
|
|
48
6
|
|
|
49
7
|
def get_robot_config(robot: str = None) -> dict:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: luckyrobots
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.73
|
|
4
4
|
Summary: Robotics-AI Training in Hyperrealistic Game Environments
|
|
5
5
|
Project-URL: Homepage, https://github.com/luckyrobots/luckyrobots
|
|
6
6
|
Project-URL: Documentation, https://luckyrobots.readthedocs.io
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
luckyrobots/__init__.py,sha256=
|
|
2
|
-
luckyrobots/client.py,sha256=
|
|
1
|
+
luckyrobots/__init__.py,sha256=cjma4V6iH-1QWWkt9n9Y9JD8LZJgmTIXP7eE9RT2ylw,469
|
|
2
|
+
luckyrobots/client.py,sha256=_OSK5WgMMcSUtSHwJRdmytB-WIJHYMazrwhkV5LSO0c,17667
|
|
3
3
|
luckyrobots/luckyrobots.py,sha256=-XkIt9h7si4GK_j5Wpknb7_h56CCWi8_d1woVMuIvxU,8957
|
|
4
|
-
luckyrobots/utils.py,sha256=
|
|
4
|
+
luckyrobots/utils.py,sha256=deh0OV3I6Tr6V1s0yWs4jtchsZK1vKySpUohLjqOF1I,1769
|
|
5
5
|
luckyrobots/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
luckyrobots/config/robots.yaml,sha256=
|
|
7
|
-
luckyrobots/engine/__init__.py,sha256
|
|
8
|
-
luckyrobots/engine/check_updates.py,sha256=KGQHrj1LB0_vexktO01xx-ZvPOYKkto4VBb3H1KV7uI,7593
|
|
9
|
-
luckyrobots/engine/download.py,sha256=vIb5T40Jk2BnfjPo0f4KgW14Om6P4_mvPpZ9JAlkVvg,4029
|
|
6
|
+
luckyrobots/config/robots.yaml,sha256=TevKlMzsZw-8k3PDf1aOG8zs18LedquS-ayVZeqyYdU,5968
|
|
7
|
+
luckyrobots/engine/__init__.py,sha256=OsyOgAnLtOfpRDHXa3Gzc1fRhJA8EfKvRkzijF6iRc8,403
|
|
10
8
|
luckyrobots/engine/manager.py,sha256=8vxSevaoXo-lw2G6y74lVc-Ms8xIELK0yLswydMiOQY,13388
|
|
11
9
|
luckyrobots/grpc/__init__.py,sha256=hPh-X_FEus2sxrfQB2fQjD754X5bMaUFTWm3mZmUhe0,153
|
|
12
10
|
luckyrobots/grpc/generated/__init__.py,sha256=u69M4E_2LwRQSD_n4wvQyMX7dryBCrIdsho59c52Crk,732
|
|
@@ -37,11 +35,9 @@ luckyrobots/grpc/proto/mujoco.proto,sha256=b7tBvYqqvcSjmqrEax6_75-P3fIHHZq63A1HV
|
|
|
37
35
|
luckyrobots/grpc/proto/scene.proto,sha256=ZypWsUzpgplGsQPP0FXJpyY4rSxgWYGsniM4vofhnak,3075
|
|
38
36
|
luckyrobots/grpc/proto/telemetry.proto,sha256=PZiUoA0bpwuvsBUdoQQdBNGWOuhlBMKAiBpwqPZiJgY,1481
|
|
39
37
|
luckyrobots/grpc/proto/viewport.proto,sha256=gunF8cIrVgvoAX8FNER6gw_u-IlRWAR1Q6ts0zUuqr8,1324
|
|
40
|
-
luckyrobots/models/__init__.py,sha256=
|
|
41
|
-
luckyrobots/models/
|
|
42
|
-
luckyrobots/
|
|
43
|
-
luckyrobots/
|
|
44
|
-
luckyrobots-0.1.
|
|
45
|
-
luckyrobots-0.1.
|
|
46
|
-
luckyrobots-0.1.72.dist-info/licenses/LICENSE,sha256=xsPYvRJPH_fW_sofTUknI_KvZOsD4-BqjSOTZqI6Nmw,1069
|
|
47
|
-
luckyrobots-0.1.72.dist-info/RECORD,,
|
|
38
|
+
luckyrobots/models/__init__.py,sha256=h_PFZ1OgJnHXfQBqO8_ULB8szCKrGA4lMfmBGZgtIDc,126
|
|
39
|
+
luckyrobots/models/observation.py,sha256=H45zargSOMR4c-zPupbXNplWBTBfGdG-oQbib203eCY,3620
|
|
40
|
+
luckyrobots-0.1.73.dist-info/METADATA,sha256=GJY8BZbJzlwk_1VRViO_DmCZKoBxQjOxsAst7baVY38,8379
|
|
41
|
+
luckyrobots-0.1.73.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
42
|
+
luckyrobots-0.1.73.dist-info/licenses/LICENSE,sha256=xsPYvRJPH_fW_sofTUknI_KvZOsD4-BqjSOTZqI6Nmw,1069
|
|
43
|
+
luckyrobots-0.1.73.dist-info/RECORD,,
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Check for LuckyEngine executable updates.
|
|
3
|
-
|
|
4
|
-
This module compares local and remote file structures to detect changes.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import logging
|
|
9
|
-
import mimetypes
|
|
10
|
-
import os
|
|
11
|
-
import platform
|
|
12
|
-
import re
|
|
13
|
-
import sys
|
|
14
|
-
import zlib
|
|
15
|
-
from typing import Optional
|
|
16
|
-
from urllib.parse import urljoin
|
|
17
|
-
|
|
18
|
-
import requests
|
|
19
|
-
|
|
20
|
-
logger = logging.getLogger("luckyrobots.engine.check_updates")
|
|
21
|
-
|
|
22
|
-
BASE_URL = "https://builds.luckyrobots.xyz/"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def get_os_type() -> str:
|
|
26
|
-
"""
|
|
27
|
-
Get the operating system type as a string.
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
"mac", "win", or "linux"
|
|
31
|
-
|
|
32
|
-
Raises:
|
|
33
|
-
ValueError: If the OS is not supported.
|
|
34
|
-
"""
|
|
35
|
-
os_type = platform.system().lower()
|
|
36
|
-
if os_type == "darwin":
|
|
37
|
-
return "mac"
|
|
38
|
-
elif os_type == "windows":
|
|
39
|
-
return "win"
|
|
40
|
-
elif os_type == "linux":
|
|
41
|
-
return "linux"
|
|
42
|
-
else:
|
|
43
|
-
raise ValueError(f"Unsupported operating system: {os_type}")
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def calculate_crc32(file_path: str) -> int:
|
|
47
|
-
"""
|
|
48
|
-
Calculate CRC32 checksum for a file.
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
file_path: Path to the file.
|
|
52
|
-
|
|
53
|
-
Returns:
|
|
54
|
-
CRC32 checksum as unsigned 32-bit integer.
|
|
55
|
-
"""
|
|
56
|
-
crc32 = 0
|
|
57
|
-
with open(file_path, "rb") as file:
|
|
58
|
-
while True:
|
|
59
|
-
data = file.read(65536) # Read in 64kb chunks
|
|
60
|
-
if not data:
|
|
61
|
-
break
|
|
62
|
-
crc32 = zlib.crc32(data, crc32)
|
|
63
|
-
return crc32 & 0xFFFFFFFF # Ensure unsigned 32-bit integer
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def scan_directory(root_path: str) -> list[dict]:
|
|
67
|
-
"""
|
|
68
|
-
Scan a directory and create a file structure representation.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
root_path: Root directory to scan.
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
List of dictionaries containing file/directory metadata.
|
|
75
|
-
"""
|
|
76
|
-
file_structure = []
|
|
77
|
-
|
|
78
|
-
for dirpath, dirnames, filenames in os.walk(root_path):
|
|
79
|
-
# Add directories
|
|
80
|
-
for dirname in dirnames:
|
|
81
|
-
dir_path = os.path.join(dirpath, dirname)
|
|
82
|
-
relative_path = os.path.relpath(dir_path, root_path)
|
|
83
|
-
file_structure.append(
|
|
84
|
-
{
|
|
85
|
-
"path": relative_path,
|
|
86
|
-
"type": "directory",
|
|
87
|
-
"size": 0,
|
|
88
|
-
"mtime": os.path.getmtime(dir_path),
|
|
89
|
-
"crc32": 0,
|
|
90
|
-
"mime_type": "directory",
|
|
91
|
-
}
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
# Add files
|
|
95
|
-
for filename in filenames:
|
|
96
|
-
file_path = os.path.join(dirpath, filename)
|
|
97
|
-
relative_path = os.path.relpath(file_path, root_path)
|
|
98
|
-
file_stat = os.stat(file_path)
|
|
99
|
-
|
|
100
|
-
# Guess the file type using mimetypes
|
|
101
|
-
file_type, _ = mimetypes.guess_type(file_path)
|
|
102
|
-
if file_type is None:
|
|
103
|
-
file_type = "application/octet-stream"
|
|
104
|
-
|
|
105
|
-
file_structure.append(
|
|
106
|
-
{
|
|
107
|
-
"path": relative_path,
|
|
108
|
-
"crc32": calculate_crc32(file_path),
|
|
109
|
-
"size": file_stat.st_size,
|
|
110
|
-
"mtime": file_stat.st_mtime,
|
|
111
|
-
"type": "file",
|
|
112
|
-
"mime_type": file_type,
|
|
113
|
-
}
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
return file_structure
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def save_json(data: dict, filename: str) -> None:
|
|
120
|
-
"""Save data to a JSON file."""
|
|
121
|
-
with open(filename, "w") as f:
|
|
122
|
-
json.dump(data, f, indent=2)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def load_json(filename: str) -> dict:
|
|
126
|
-
"""Load data from a JSON file."""
|
|
127
|
-
with open(filename, "r") as f:
|
|
128
|
-
return json.load(f)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def clean_path(path: str) -> tuple[str, Optional[str]]:
|
|
132
|
-
"""
|
|
133
|
-
Clean and parse a path string.
|
|
134
|
-
|
|
135
|
-
Args:
|
|
136
|
-
path: Path string to clean.
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
Tuple of (directory_path, attribute_name).
|
|
140
|
-
"""
|
|
141
|
-
# Remove ['children'] and quotes, replace '][' with '/'
|
|
142
|
-
cleaned = re.sub(r"\['children'\]", "", path).replace("']['", "/").strip("[]'")
|
|
143
|
-
# Replace remaining single quotes with nothing
|
|
144
|
-
cleaned = cleaned.replace("'", "")
|
|
145
|
-
# Split the path and the attribute that changed
|
|
146
|
-
parts = cleaned.rsplit("/", 1)
|
|
147
|
-
return parts[0], parts[1] if len(parts) > 1 else None
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def compare_structures(
|
|
151
|
-
client_structure: list[dict], server_structure: list[dict]
|
|
152
|
-
) -> list[dict]:
|
|
153
|
-
"""
|
|
154
|
-
Compare two file structures and return list of changes.
|
|
155
|
-
|
|
156
|
-
Args:
|
|
157
|
-
client_structure: Local file structure.
|
|
158
|
-
server_structure: Remote file structure.
|
|
159
|
-
|
|
160
|
-
Returns:
|
|
161
|
-
List of changes (new, modified, deleted files).
|
|
162
|
-
"""
|
|
163
|
-
dict1 = {item["path"]: item for item in client_structure}
|
|
164
|
-
dict2 = {item["path"]: item for item in server_structure}
|
|
165
|
-
|
|
166
|
-
result = []
|
|
167
|
-
|
|
168
|
-
# Check for new and modified items
|
|
169
|
-
for path, item in dict2.items():
|
|
170
|
-
if path not in dict1:
|
|
171
|
-
item["change_type"] = "new_file"
|
|
172
|
-
result.append(item)
|
|
173
|
-
elif item["type"] == "file" and item["crc32"] != dict1[path]["crc32"]:
|
|
174
|
-
item["change_type"] = "modified"
|
|
175
|
-
result.append(item)
|
|
176
|
-
|
|
177
|
-
# Check for deleted items
|
|
178
|
-
for path, item in dict1.items():
|
|
179
|
-
if path not in dict2:
|
|
180
|
-
item["change_type"] = "deleted"
|
|
181
|
-
result.append(item)
|
|
182
|
-
|
|
183
|
-
# Remove hashmap.json from changes (it's metadata)
|
|
184
|
-
result = [item for item in result if item["path"] != "hashmap.json"]
|
|
185
|
-
|
|
186
|
-
return result
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def scan_server(server_path: str) -> None:
|
|
190
|
-
"""
|
|
191
|
-
Scan server directory structure and save hashmap files.
|
|
192
|
-
|
|
193
|
-
Args:
|
|
194
|
-
server_path: Path to server root directory.
|
|
195
|
-
"""
|
|
196
|
-
mac_path = os.path.join(server_path, "mac")
|
|
197
|
-
win_path = os.path.join(server_path, "win")
|
|
198
|
-
linux_path = os.path.join(server_path, "linux")
|
|
199
|
-
|
|
200
|
-
mac_structure = scan_directory(mac_path)
|
|
201
|
-
win_structure = scan_directory(win_path)
|
|
202
|
-
linux_structure = scan_directory(linux_path)
|
|
203
|
-
|
|
204
|
-
save_json(mac_structure, os.path.join(server_path, "mac/hashmap.json"))
|
|
205
|
-
save_json(win_structure, os.path.join(server_path, "win/hashmap.json"))
|
|
206
|
-
save_json(linux_structure, os.path.join(server_path, "linux/hashmap.json"))
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def check_updates(root_path: str, base_url: Optional[str] = None) -> list[dict]:
|
|
210
|
-
"""
|
|
211
|
-
Check for updates by comparing local and remote file structures.
|
|
212
|
-
|
|
213
|
-
Args:
|
|
214
|
-
root_path: Local root directory to check.
|
|
215
|
-
base_url: Base URL for remote server (uses default if None).
|
|
216
|
-
|
|
217
|
-
Returns:
|
|
218
|
-
List of changes detected.
|
|
219
|
-
"""
|
|
220
|
-
if base_url is None:
|
|
221
|
-
base_url = BASE_URL
|
|
222
|
-
|
|
223
|
-
os_type = get_os_type()
|
|
224
|
-
url = urljoin(base_url, f"{os_type}/hashmap.json")
|
|
225
|
-
|
|
226
|
-
# Download the JSON file
|
|
227
|
-
try:
|
|
228
|
-
response = requests.get(url, timeout=10)
|
|
229
|
-
response.raise_for_status()
|
|
230
|
-
server_structure = response.json()
|
|
231
|
-
except requests.RequestException as e:
|
|
232
|
-
logger.warning(f"Error downloading JSON file: {e}")
|
|
233
|
-
server_structure = None
|
|
234
|
-
|
|
235
|
-
if server_structure is None:
|
|
236
|
-
logger.info("Using local scan as no remote file could be downloaded.")
|
|
237
|
-
server_structure = {}
|
|
238
|
-
|
|
239
|
-
# Scan the directory and create a new file structure
|
|
240
|
-
client_structure = scan_directory(root_path)
|
|
241
|
-
|
|
242
|
-
# Compare and return changes
|
|
243
|
-
if server_structure:
|
|
244
|
-
changes = compare_structures(client_structure, server_structure)
|
|
245
|
-
return changes
|
|
246
|
-
else:
|
|
247
|
-
logger.info("No server structure available for comparison.")
|
|
248
|
-
return []
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if __name__ == "__main__":
|
|
252
|
-
# This is used as a cron job on the main server to keep file structures up to date
|
|
253
|
-
lr_server_root = None
|
|
254
|
-
|
|
255
|
-
for arg in sys.argv:
|
|
256
|
-
if arg.startswith("--lr-server-root="):
|
|
257
|
-
lr_server_root = arg.split("=", 1)[1]
|
|
258
|
-
break
|
|
259
|
-
|
|
260
|
-
if lr_server_root:
|
|
261
|
-
logger.info(f"Scanning server at {lr_server_root}")
|
|
262
|
-
scan_server(lr_server_root)
|
|
263
|
-
else:
|
|
264
|
-
logger.error("--lr-server-root argument required")
|