uipath-robot 0.0.3__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.
@@ -0,0 +1,472 @@
1
+ """UiPath Robot Package Initialization."""
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import uuid
8
+ import zipfile
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from dotenv import load_dotenv
13
+
14
+ from uipath.robot.infra import get_logger, init_logger
15
+ from uipath.robot.models import (
16
+ Command,
17
+ HeartbeatData,
18
+ HeartbeatResponse,
19
+ JobError,
20
+ JobState,
21
+ SessionState,
22
+ )
23
+ from uipath.robot.services import OrchestratorService
24
+
25
+
26
+ class Robot:
27
+ """Main Robot class to manage interactions with UiPath Orchestrator."""
28
+
29
+ def __init__(self, verbose: bool = False):
30
+ """Initialize the Robot with necessary services.
31
+
32
+ Args:
33
+ verbose: Enable verbose logging
34
+ """
35
+ self.logger = get_logger(verbose)
36
+ self.orchestrator = OrchestratorService()
37
+ self.heartbeat_interval = 5 # seconds
38
+ self.license_key: str | None = None
39
+
40
+ base_dir = Path.cwd() / ".uipath"
41
+ self.packages_dir = base_dir / "packages"
42
+ self.processes_dir = base_dir / "processes"
43
+
44
+ self.packages_dir.mkdir(parents=True, exist_ok=True)
45
+ self.processes_dir.mkdir(parents=True, exist_ok=True)
46
+
47
+ async def initialize(self) -> bool:
48
+ """Initialize the service by getting connection data and license key.
49
+
50
+ Returns:
51
+ True if initialization successful, False otherwise
52
+ """
53
+ self.logger.info("Initializing robot service")
54
+
55
+ self.license_key = await self.orchestrator.get_shared_connection_data()
56
+
57
+ if not self.license_key:
58
+ self.logger.error("Failed to get connection data")
59
+ return False
60
+
61
+ self.logger.success(f"Connected (key: {self.license_key[:10]}...)")
62
+ return True
63
+
64
+ def get_package_path(self, package_id: str, package_version: str) -> Path:
65
+ """Get the path for a package zip file."""
66
+ return self.packages_dir / f"{package_id}_{package_version}.zip"
67
+
68
+ def get_process_path(self, package_id: str, package_version: str) -> Path:
69
+ """Get the path for an extracted process."""
70
+ return self.processes_dir / f"{package_id}_{package_version}"
71
+
72
+ async def download_and_setup_package(
73
+ self, package_id: str, package_version: str, feed_id: str | None = None
74
+ ) -> Path | None:
75
+ """Download package if needed, extract it, and setup environment.
76
+
77
+ Args:
78
+ package_id: The package identifier
79
+ package_version: The package version
80
+ feed_id: Optional feed identifier
81
+
82
+ Returns:
83
+ Path to the extracted process directory, or None if failed
84
+ """
85
+ package_path = self.get_package_path(package_id, package_version)
86
+ process_path = self.get_process_path(package_id, package_version)
87
+
88
+ # Check if already downloaded and extracted
89
+ if process_path.exists():
90
+ self.logger.package_status(package_id, package_version, "cached")
91
+ return process_path
92
+
93
+ # Download package if not exists
94
+ if not package_path.exists():
95
+ self.logger.package_status(package_id, package_version, "downloading")
96
+ success = await self.orchestrator.download_package(
97
+ package_id,
98
+ package_version,
99
+ package_path,
100
+ feed_id,
101
+ )
102
+ if not success:
103
+ self.logger.error("Failed to download package")
104
+ return None
105
+ self.logger.debug(f"Downloaded to {package_path}", indent=1)
106
+
107
+ # Extract package - extract only the 'content' folder
108
+ self.logger.package_status(package_id, package_version, "extracting")
109
+ try:
110
+ process_path.mkdir(parents=True, exist_ok=True)
111
+
112
+ with zipfile.ZipFile(package_path, "r") as zip_ref:
113
+ # Get all files in the 'content' folder
114
+ content_files = [
115
+ f for f in zip_ref.namelist() if f.startswith("content/")
116
+ ]
117
+
118
+ if not content_files:
119
+ self.logger.error("No 'content' folder found in package")
120
+ return None
121
+
122
+ # Extract each file, removing the 'content/' prefix
123
+ for file in content_files:
124
+ if file == "content/":
125
+ continue
126
+
127
+ # Remove 'content/' prefix from the path
128
+ target_path = file[len("content/") :]
129
+
130
+ # Extract to process_path with adjusted path
131
+ if target_path:
132
+ source = zip_ref.open(file)
133
+ target = process_path / target_path
134
+ target.parent.mkdir(parents=True, exist_ok=True)
135
+
136
+ with source, open(target, "wb") as target_file:
137
+ target_file.write(source.read())
138
+
139
+ self.logger.package_status(package_id, package_version, "extracted")
140
+ except Exception as e:
141
+ self.logger.error(f"Failed to extract: {e}")
142
+ return None
143
+
144
+ # Setup virtual environment only if pyproject.toml exists
145
+ pyproject_path = process_path / "pyproject.toml"
146
+ if pyproject_path.exists():
147
+ self.logger.environment_setup("creating")
148
+ venv_path = process_path / ".venv"
149
+ try:
150
+ subprocess.run(
151
+ ["uv", "venv", str(venv_path)],
152
+ cwd=process_path,
153
+ check=True,
154
+ capture_output=True,
155
+ )
156
+
157
+ # Copy existing environment and add custom vars
158
+ env_with_temp = os.environ.copy()
159
+ env_with_temp["VIRTUAL_ENV"] = str(venv_path)
160
+ env_with_temp["TMPDIR"] = str(process_path / "temp") # Linux/Mac
161
+ env_with_temp["TEMP"] = str(process_path / "temp") # Windows
162
+ env_with_temp["TMP"] = str(process_path / "temp") # Windows
163
+
164
+ (process_path / "temp").mkdir(exist_ok=True)
165
+
166
+ self.logger.environment_setup("syncing")
167
+ subprocess.run(
168
+ ["uv", "sync"],
169
+ cwd=process_path,
170
+ env=env_with_temp,
171
+ check=True,
172
+ capture_output=True,
173
+ text=True,
174
+ )
175
+ self.logger.environment_setup("ready")
176
+
177
+ except subprocess.CalledProcessError as e:
178
+ self.logger.error(f"Environment setup failed: {e}")
179
+ self.logger.debug(f"stdout: {e.stdout}", indent=1)
180
+ self.logger.debug(f"stderr: {e.stderr}", indent=1)
181
+ return None
182
+ except Exception as e:
183
+ self.logger.error(f"Unexpected error: {e}")
184
+ return None
185
+ else:
186
+ self.logger.environment_setup("skipped")
187
+
188
+ return process_path
189
+
190
+ async def execute_process(self, process_path: Path, command: Command) -> None:
191
+ """Execute a process using uipath run.
192
+
193
+ Args:
194
+ process_path: Path to the process directory
195
+ command: The command data containing job details
196
+ """
197
+ assert self.license_key is not None
198
+ self.logger.process_execution("starting")
199
+ try:
200
+ # Create __uipath directory and uipath.json
201
+ uipath_dir = process_path / "__uipath"
202
+ uipath_dir.mkdir(exist_ok=True)
203
+
204
+ uipath_config = {
205
+ "runtime": self._parse_json_or_empty(command.data.internal_arguments),
206
+ "fpsProperties": self._parse_json_or_empty(command.data.fps_properties),
207
+ "fpsContext": self._parse_json_or_empty(command.data.fps_context),
208
+ }
209
+
210
+ uipath_json_path = uipath_dir / "uipath.json"
211
+ with open(uipath_json_path, "w") as f:
212
+ json.dump(uipath_config, f, indent=2)
213
+
214
+ # Prepare environment variables
215
+ env = os.environ.copy()
216
+ env["UIPATH_JOB_KEY"] = command.data.job_key
217
+ env["UIPATH_FOLDER_KEY"] = command.data.folder_key
218
+ env["UIPATH_FOLDER_PATH"] = command.data.fully_qualified_folder_name
219
+ env["UIPATH_PROCESS_UUID"] = command.data.process_key
220
+ env["VIRTUAL_ENV"] = str(process_path / ".venv")
221
+
222
+ trace_id = command.data.trace_id or str(uuid.uuid4())
223
+ env["UIPATH_TRACE_ID"] = trace_id
224
+ if command.data.parent_span_id:
225
+ env["UIPATH_PARENT_SPAN_ID"] = command.data.parent_span_id
226
+ if command.data.root_span_id:
227
+ env["UIPATH_ROOT_SPAN_ID"] = command.data.root_span_id
228
+
229
+ # Ensure required env vars are present
230
+ required_vars = [
231
+ "UIPATH_ACCESS_TOKEN",
232
+ "UIPATH_URL",
233
+ "UIPATH_TENANT_ID",
234
+ "UIPATH_ORGANIZATION_ID",
235
+ ]
236
+
237
+ missing_vars = [var for var in required_vars if not env.get(var)]
238
+ if missing_vars:
239
+ self.logger.error(f"Missing env vars: {', '.join(missing_vars)}")
240
+ return
241
+
242
+ await self.orchestrator.submit_job_state(
243
+ heartbeats=[
244
+ HeartbeatData(
245
+ robot_key=command.robot_key,
246
+ robot_state=SessionState.BUSY,
247
+ process_key=command.data.process_key,
248
+ job_key=command.data.job_key,
249
+ job_state=JobState.RUNNING,
250
+ trace_id=trace_id,
251
+ )
252
+ ],
253
+ license_key=self.license_key,
254
+ )
255
+
256
+ self.logger.process_execution("running")
257
+
258
+ cmd = ["uv", "run", "uipath", "run"]
259
+ if command.data.entry_point_path:
260
+ cmd.extend([command.data.entry_point_path])
261
+ if command.data.input_arguments:
262
+ cmd.extend([command.data.input_arguments])
263
+
264
+ result = subprocess.run(
265
+ cmd,
266
+ cwd=process_path,
267
+ env=env,
268
+ check=True,
269
+ capture_output=True,
270
+ text=True,
271
+ )
272
+
273
+ output_file = process_path / "__uipath" / "output.json"
274
+ if output_file.exists():
275
+ output_data: dict[str, Any] = {}
276
+ with open(output_file, "r") as f:
277
+ output_data = json.load(f)
278
+
279
+ status = output_data.get("status", "successful")
280
+ output_args = json.dumps(output_data.get("output", {}))
281
+
282
+ if status == "successful":
283
+ job_state = JobState.SUCCESSFUL
284
+ error = None
285
+ self.logger.process_execution("success", result.stdout.strip())
286
+ else:
287
+ job_state = JobState.FAULTED
288
+ error_data: dict[str, Any] = output_data.get("error", {})
289
+ error = JobError(
290
+ code=error_data.get("code", "PYTHON.PROCESS_FAILED"),
291
+ title=error_data.get("title", "Process Failed"),
292
+ category=error_data.get("category", "USER"),
293
+ detail=error_data.get(
294
+ "detail", "Process completed with failure status"
295
+ ),
296
+ )
297
+ self.logger.process_execution("failed", error_data.get("detail"))
298
+ else:
299
+ # No output file, assume success
300
+ job_state = JobState.SUCCESSFUL
301
+ output_args = "{}"
302
+ error = None
303
+ self.logger.process_execution("success", result.stdout.strip())
304
+
305
+ await self.orchestrator.submit_job_state(
306
+ heartbeats=[
307
+ HeartbeatData(
308
+ robot_key=command.robot_key,
309
+ robot_state=SessionState.AVAILABLE,
310
+ process_key=command.data.process_key,
311
+ job_key=command.data.job_key,
312
+ job_state=job_state,
313
+ output_arguments=output_args,
314
+ error=error,
315
+ )
316
+ ],
317
+ license_key=self.license_key,
318
+ )
319
+
320
+ except subprocess.CalledProcessError as e:
321
+ self.logger.process_execution("failed", e.stderr)
322
+ await self.orchestrator.submit_job_state(
323
+ heartbeats=[
324
+ HeartbeatData(
325
+ robot_key=command.robot_key,
326
+ robot_state=SessionState.AVAILABLE,
327
+ process_key=command.data.process_key,
328
+ job_key=command.data.job_key,
329
+ job_state=JobState.FAULTED,
330
+ error=JobError(
331
+ code="PYTHON.AGENT_EXECUTION_FAILED",
332
+ title="Agent Execution Failed",
333
+ category="SYSTEM",
334
+ detail=e.stderr,
335
+ ),
336
+ )
337
+ ],
338
+ license_key=self.license_key,
339
+ )
340
+ except Exception as e:
341
+ self.logger.process_execution("failed", str(e))
342
+ await self.orchestrator.submit_job_state(
343
+ heartbeats=[
344
+ HeartbeatData(
345
+ robot_key=command.robot_key,
346
+ robot_state=SessionState.AVAILABLE,
347
+ process_key=command.data.process_key,
348
+ job_key=command.data.job_key,
349
+ job_state=JobState.FAULTED,
350
+ error=JobError(
351
+ code="PYTHON.AGENT_EXECUTION_FAILED",
352
+ title="Agent Execution Failed",
353
+ category="SYSTEM",
354
+ detail=str(e),
355
+ ),
356
+ )
357
+ ],
358
+ license_key=self.license_key,
359
+ )
360
+
361
+ async def process_commands(self, response: HeartbeatResponse) -> None:
362
+ """Process commands from heartbeat response.
363
+
364
+ Args:
365
+ response: The heartbeat response containing commands
366
+ """
367
+ for command in response.commands:
368
+ if command.data.type == "StartProcess":
369
+ self.logger.job_start(
370
+ command.data.job_key,
371
+ command.data.package_id,
372
+ command.data.package_version,
373
+ )
374
+
375
+ # Download and setup package
376
+ process_path = await self.download_and_setup_package(
377
+ command.data.package_id,
378
+ command.data.package_version,
379
+ command.data.feed_id,
380
+ )
381
+
382
+ if process_path:
383
+ # Execute the process asynchronously without blocking
384
+ asyncio.create_task(
385
+ self.execute_process(
386
+ process_path,
387
+ command,
388
+ )
389
+ )
390
+ else:
391
+ self.logger.error("Failed to setup package")
392
+
393
+ def _parse_json_or_empty(self, raw_json: str | None) -> dict[str, Any]:
394
+ """Parse JSON string or return empty dict.
395
+
396
+ Args:
397
+ raw_json: JSON string to parse
398
+
399
+ Returns:
400
+ Parsed JSON as dict or empty dict if parsing fails
401
+ """
402
+ if not raw_json or not raw_json.strip():
403
+ return {}
404
+
405
+ try:
406
+ return json.loads(raw_json)
407
+ except (json.JSONDecodeError, Exception):
408
+ return {}
409
+
410
+ async def run(self) -> None:
411
+ """Main loop: Initialize once, then send heartbeats every 10 seconds."""
412
+ self.logger.section("UiPath Robot Service")
413
+
414
+ if not await self.initialize():
415
+ self.logger.error("Initialization failed")
416
+ return
417
+
418
+ assert self.license_key is not None
419
+
420
+ await self.orchestrator.start_service(self.license_key)
421
+ self.logger.success("Service started")
422
+
423
+ self.logger.info(f"Listening for jobs (heartbeat: {self.heartbeat_interval}s)")
424
+
425
+ try:
426
+ while True:
427
+ response = await self.orchestrator.heartbeat(self.license_key)
428
+ self.logger.heartbeat()
429
+
430
+ if response and response.commands:
431
+ await self.process_commands(response)
432
+
433
+ await asyncio.sleep(self.heartbeat_interval)
434
+ finally:
435
+ self.logger.info("Stopping service...")
436
+ await self.orchestrator.stop_service(self.license_key)
437
+
438
+
439
+ async def start_robot(verbose: bool = False) -> None:
440
+ """Run the robot service.
441
+
442
+ Args:
443
+ verbose: Enable verbose logging
444
+ """
445
+ robot = Robot(verbose=verbose)
446
+ await robot.run()
447
+
448
+
449
+ def main() -> None:
450
+ """Main entry point for running the robot service."""
451
+ import argparse
452
+
453
+ parser = argparse.ArgumentParser(description="UiPath Robot Service")
454
+ parser.add_argument(
455
+ "-v",
456
+ "--verbose",
457
+ action="store_true",
458
+ help="Enable verbose logging",
459
+ )
460
+ args = parser.parse_args()
461
+
462
+ load_dotenv()
463
+ init_logger(verbose=args.verbose)
464
+
465
+ try:
466
+ asyncio.run(start_robot(verbose=args.verbose))
467
+ except KeyboardInterrupt:
468
+ pass
469
+
470
+
471
+ if __name__ == "__main__":
472
+ main()
@@ -0,0 +1,5 @@
1
+ """UiPath Robot Infrastructure Package."""
2
+
3
+ from uipath.robot.infra.logger import get_logger, init_logger
4
+
5
+ __all__ = ["get_logger", "init_logger"]
@@ -0,0 +1,181 @@
1
+ """CLI Logger for UiPath Robot."""
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+
6
+
7
+ class LogLevel(Enum):
8
+ """Log levels with visual indicators."""
9
+
10
+ INFO = ("●", "\033[36m") # Cyan dot
11
+ SUCCESS = ("✓", "\033[32m") # Green checkmark
12
+ WARNING = ("⚠", "\033[33m") # Yellow warning
13
+ ERROR = ("✗", "\033[31m") # Red X
14
+ DEBUG = ("·", "\033[90m") # Gray dot
15
+ SYSTEM = ("→", "\033[35m") # Magenta arrow
16
+
17
+
18
+ class CLILogger:
19
+ """Clean CLI logger with structured output."""
20
+
21
+ RESET = "\033[0m"
22
+ BOLD = "\033[1m"
23
+ DIM = "\033[2m"
24
+
25
+ def __init__(self, verbose: bool = False):
26
+ """Initialize logger.
27
+
28
+ Args:
29
+ verbose: If True, show debug messages
30
+ """
31
+ self.verbose: bool = verbose
32
+ self._last_section: str | None = None
33
+
34
+ def _format_time(self) -> str:
35
+ """Get formatted timestamp."""
36
+ return datetime.now().strftime("%H:%M:%S")
37
+
38
+ def _log(self, level: LogLevel, message: str, indent: int = 0):
39
+ """Internal log method.
40
+
41
+ Args:
42
+ level: The log level
43
+ message: The message to log
44
+ indent: Indentation level (0-2)
45
+ """
46
+ if level == LogLevel.DEBUG and not self.verbose:
47
+ return
48
+
49
+ symbol, color = level.value
50
+ indent_str = " " * indent
51
+ time_str = f"{self.DIM}{self._format_time()}{self.RESET}"
52
+
53
+ print(f"{time_str} {color}{symbol}{self.RESET} {indent_str}{message}")
54
+
55
+ def section(self, title: str):
56
+ """Print a section header.
57
+
58
+ Args:
59
+ title: Section title
60
+ """
61
+ if self._last_section:
62
+ print() # Add spacing between sections
63
+ print(f"\n{self.BOLD}{title}{self.RESET}")
64
+ print("─" * len(title))
65
+ self._last_section = title
66
+
67
+ def info(self, message: str, indent: int = 0):
68
+ """Log info message."""
69
+ self._log(LogLevel.INFO, message, indent)
70
+
71
+ def success(self, message: str, indent: int = 0):
72
+ """Log success message."""
73
+ self._log(LogLevel.SUCCESS, message, indent)
74
+
75
+ def warning(self, message: str, indent: int = 0):
76
+ """Log warning message."""
77
+ self._log(LogLevel.WARNING, message, indent)
78
+
79
+ def error(self, message: str, indent: int = 0):
80
+ """Log error message."""
81
+ self._log(LogLevel.ERROR, message, indent)
82
+
83
+ def debug(self, message: str, indent: int = 0):
84
+ """Log debug message (only if verbose)."""
85
+ self._log(LogLevel.DEBUG, message, indent)
86
+
87
+ def system(self, message: str, indent: int = 0):
88
+ """Log system/internal message."""
89
+ self._log(LogLevel.SYSTEM, message, indent)
90
+
91
+ def job_start(self, job_key: str, package: str, version: str):
92
+ """Log job start with formatted output."""
93
+ self.section(f"Job {job_key[:8]}...")
94
+ self.info(f"Package: {self.BOLD}{package}{self.RESET} v{version}")
95
+
96
+ def heartbeat(self, count: int | None = None):
97
+ """Log heartbeat in a minimal way."""
98
+ if self.verbose:
99
+ msg = f"Heartbeat {count}" if count else "Heartbeat"
100
+ self.debug(msg)
101
+
102
+ def package_status(self, package: str, version: str, status: str):
103
+ """Log package download/extraction status.
104
+
105
+ Args:
106
+ package: Package ID
107
+ version: Package version
108
+ status: Status message (downloading/extracting/cached)
109
+ """
110
+ if status == "cached":
111
+ self.debug(f"Using cached {package}:{version}")
112
+ elif status == "downloading":
113
+ self.info(f"Downloading {package}:{version}")
114
+ elif status == "extracting":
115
+ self.info("Extracting package", indent=1)
116
+ elif status == "extracted":
117
+ self.success("Package ready", indent=1)
118
+
119
+ def environment_setup(self, status: str):
120
+ """Log environment setup status.
121
+
122
+ Args:
123
+ status: Status (creating/syncing/ready/skipped)
124
+ """
125
+ if status == "creating":
126
+ self.info("Creating virtual environment", indent=1)
127
+ elif status == "syncing":
128
+ self.info("Installing dependencies", indent=1)
129
+ elif status == "ready":
130
+ self.success("Environment ready", indent=1)
131
+ elif status == "skipped":
132
+ self.debug("No pyproject.toml, skipping venv", indent=1)
133
+
134
+ def process_execution(self, status: str, detail: str | None = None):
135
+ """Log process execution status.
136
+
137
+ Args:
138
+ status: Status (starting/running/success/failed)
139
+ detail: Optional detail message
140
+ """
141
+ if status == "starting":
142
+ self.info("Starting process execution")
143
+ elif status == "running":
144
+ self.system("Process running", indent=1)
145
+ elif status == "success":
146
+ self.success("Process completed successfully")
147
+ if detail:
148
+ self.debug(f"Output: {detail}", indent=1)
149
+ elif status == "failed":
150
+ self.error("Process execution failed")
151
+ if detail:
152
+ self.error(detail, indent=1)
153
+
154
+
155
+ # Global logger instance
156
+ _logger: CLILogger | None = None
157
+
158
+
159
+ def get_logger(verbose: bool = False) -> CLILogger:
160
+ """Get or create the global logger instance.
161
+
162
+ Args:
163
+ verbose: Enable verbose logging
164
+
165
+ Returns:
166
+ The CLI logger instance
167
+ """
168
+ global _logger
169
+ if _logger is None:
170
+ _logger = CLILogger(verbose=verbose)
171
+ return _logger
172
+
173
+
174
+ def init_logger(verbose: bool = False):
175
+ """Initialize the global logger.
176
+
177
+ Args:
178
+ verbose: Enable verbose logging
179
+ """
180
+ global _logger
181
+ _logger = CLILogger(verbose=verbose)
@@ -0,0 +1,21 @@
1
+ """Models for the robot heartbeat functionality."""
2
+
3
+ from .heartbeat import (
4
+ Command,
5
+ CommandData,
6
+ HeartbeatData,
7
+ HeartbeatResponse,
8
+ JobError,
9
+ JobState,
10
+ SessionState,
11
+ )
12
+
13
+ __all__ = [
14
+ "Command",
15
+ "CommandData",
16
+ "HeartbeatData",
17
+ "JobError",
18
+ "JobState",
19
+ "SessionState",
20
+ "HeartbeatResponse",
21
+ ]
@@ -0,0 +1,166 @@
1
+ """Models for the heartbeat response from Orchestrator."""
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field
7
+
8
+
9
+ class JobState(str, Enum):
10
+ """Job state enumeration."""
11
+
12
+ PENDING = "Pending"
13
+ RUNNING = "Running"
14
+ SUCCESSFUL = "Successful"
15
+ FAULTED = "Faulted"
16
+ STOPPING = "Stopping"
17
+ TERMINATING = "Terminating"
18
+ STOPPED = "Stopped"
19
+ SUSPENDED = "Suspended"
20
+ RESUMED = "Resumed"
21
+
22
+
23
+ class SessionState(str, Enum):
24
+ """Robot session state enumeration."""
25
+
26
+ AVAILABLE = "Available"
27
+ BUSY = "Busy"
28
+ DISCONNECTED = "Disconnected"
29
+ UNRESPONSIVE = "Unresponsive"
30
+
31
+
32
+ class ExecutionSettings(BaseModel):
33
+ """Execution settings for the robot."""
34
+
35
+ model_config = ConfigDict(extra="allow")
36
+
37
+
38
+ class AuthSettings(BaseModel):
39
+ """Authentication settings for the robot."""
40
+
41
+ model_config = ConfigDict(extra="allow")
42
+
43
+
44
+ class CommandData(BaseModel):
45
+ """Data payload for a robot command."""
46
+
47
+ model_config = ConfigDict(
48
+ extra="allow", validate_by_name=True, validate_by_alias=True
49
+ )
50
+
51
+ process_settings: Any = Field(None, alias="processSettings")
52
+ video_recording_settings: Any = Field(None, alias="videoRecordingSettings")
53
+ package_id: str = Field(alias="packageId")
54
+ package_version: str = Field(alias="packageVersion")
55
+ target_framework: str = Field(alias="targetFramework")
56
+ username: str | None = None
57
+ password: str | None = None
58
+ credential_connection_data: Any = Field(None, alias="credentialConnectionData")
59
+ credential_type: int = Field(alias="credentialType")
60
+ process_name: str = Field(alias="processName")
61
+ persistence_id: str | None = Field(None, alias="persistenceId")
62
+ resume_version: str | None = Field(None, alias="resumeVersion")
63
+ resume_source: str | None = Field(None, alias="resumeSource")
64
+ suspend_blob_type: str | None = Field(None, alias="suspendBlobType")
65
+ input_arguments: str = Field(alias="inputArguments")
66
+ input_file: str | None = Field(None, alias="inputFile")
67
+ default_input_arguments: str | None = Field(None, alias="defaultInputArguments")
68
+ environment_variables: str | None = Field(None, alias="environmentVariables")
69
+ internal_arguments: str = Field(alias="internalArguments")
70
+ entry_point_path: str = Field(alias="entryPointPath")
71
+ feed_id: str | None = Field(alias="feedId")
72
+ feed_url: str = Field(alias="feedUrl")
73
+ requires_user_interaction: bool = Field(alias="requiresUserInteraction")
74
+ source: str
75
+ profiling_options: dict[str, Any] | None = Field(
76
+ alias="profilingOptions", default=None
77
+ )
78
+ job_source: str = Field(alias="jobSource")
79
+ job_key: str = Field(alias="jobKey")
80
+ process_key: str = Field(alias="processKey")
81
+ folder_id: int = Field(alias="folderId")
82
+ fully_qualified_folder_name: str = Field(alias="fullyQualifiedFolderName")
83
+ folder_key: str = Field(alias="folderKey")
84
+ folder_path: str = Field(alias="folderPath")
85
+ fps_context: str | None = Field(None, alias="fpsContext")
86
+ fps_properties: str | None = Field(None, alias="fpsProperties")
87
+ trace_id: str | None = Field(None, alias="traceId")
88
+ parent_span_id: str | None = Field(None, alias="parentSpanId")
89
+ root_span_id: str | None = Field(None, alias="rootSpanId")
90
+ id: str
91
+ type: str
92
+
93
+
94
+ class UserDetails(BaseModel):
95
+ """User details associated with the command."""
96
+
97
+ model_config = ConfigDict(extra="allow")
98
+
99
+ key: str
100
+ email: str
101
+
102
+
103
+ class Command(BaseModel):
104
+ """Robot command from Orchestrator."""
105
+
106
+ model_config = ConfigDict(
107
+ extra="allow", validate_by_name=True, validate_by_alias=True
108
+ )
109
+
110
+ robot_key: str = Field(alias="robotKey")
111
+ username: str | None = None
112
+ robot_name: str = Field(alias="robotName")
113
+ robot_type: int = Field(alias="robotType")
114
+ machine_id: int = Field(alias="machineId")
115
+ has_license: bool = Field(alias="hasLicense")
116
+ is_external_licensed: bool = Field(alias="isExternalLicensed")
117
+ execution_settings: ExecutionSettings = Field(alias="executionSettings")
118
+ auth_settings: AuthSettings = Field(alias="authSettings")
119
+ data: CommandData
120
+ user_details: UserDetails = Field(alias="userDetails")
121
+
122
+
123
+ class HeartbeatResponse(BaseModel):
124
+ """Response from the heartbeat endpoint containing robot commands."""
125
+
126
+ model_config = ConfigDict(
127
+ extra="allow", validate_by_name=True, validate_by_alias=True
128
+ )
129
+
130
+ commands: list[Command]
131
+ service_settings_stamp: str = Field(alias="serviceSettingsStamp")
132
+ is_unattended: bool = Field(alias="isUnattended")
133
+ is_unattended_licensed: bool = Field(alias="isUnattendedLicensed")
134
+
135
+
136
+ class JobError(BaseModel):
137
+ """Job error details."""
138
+
139
+ model_config = ConfigDict(
140
+ extra="allow", validate_by_name=True, validate_by_alias=True
141
+ )
142
+
143
+ code: str
144
+ title: str
145
+ category: str
146
+ detail: str | None = None
147
+
148
+
149
+ class HeartbeatData(BaseModel):
150
+ """Heartbeat data for job state submission."""
151
+
152
+ model_config = ConfigDict(
153
+ extra="allow", validate_by_name=True, validate_by_alias=True
154
+ )
155
+
156
+ robot_key: str = Field(alias="robotKey")
157
+ job_state: JobState = Field(alias="jobState")
158
+ job_key: str = Field(alias="jobKey")
159
+ info: str | None = None
160
+ process_key: str = Field(alias="processKey")
161
+ output_arguments: str | None = Field(None, alias="outputArguments")
162
+ robot_state: SessionState = Field(alias="robotState")
163
+ error: JobError | None = Field(None, alias="error")
164
+ trace_id: str | None = Field(None, alias="traceId")
165
+ parent_span_id: str | None = Field(None, alias="parentSpanId")
166
+ root_span_id: str | None = Field(None, alias="rootSpanId")
uipath/robot/py.typed ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ """Init file for robot services."""
2
+
3
+ from .identity import IdentityService
4
+ from .orchestrator import OrchestratorService
5
+
6
+ __all__ = ["IdentityService", "OrchestratorService"]
@@ -0,0 +1,79 @@
1
+ """Module for handling machine authentication and token management."""
2
+
3
+ import os
4
+ from datetime import datetime, timedelta
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+
10
+ class IdentityService:
11
+ """Service for handling machine authentication and token management."""
12
+
13
+ def __init__(self):
14
+ """Initialize the IdentityService with environment variables."""
15
+ self.client_id = os.getenv("MACHINE_CLIENT_ID")
16
+ self.client_secret = os.getenv("MACHINE_CLIENT_SECRET")
17
+ self.base_url = os.getenv("UIPATH_URL")
18
+ self.organization_id = os.getenv("UIPATH_ORGANIZATION_ID")
19
+ self.tenant_id = os.getenv("UIPATH_TENANT_ID")
20
+
21
+ if not all(
22
+ [
23
+ self.client_id,
24
+ self.client_secret,
25
+ self.base_url,
26
+ self.organization_id,
27
+ self.tenant_id,
28
+ ]
29
+ ):
30
+ raise ValueError(
31
+ "Missing required environment variables: MACHINE_CLIENT_ID, MACHINE_CLIENT_SECRET, UIPATH_URL, UIPATH_ORGANIZATION_ID, UIPATH_TENANT_ID"
32
+ )
33
+
34
+ self.access_token: str | None = None
35
+ self.token_expiry: datetime | None = None
36
+
37
+ async def get_token(self) -> str:
38
+ """Get Service-to-Service access token using client credentials."""
39
+ assert self.base_url is not None
40
+
41
+ token_endpoint = f"{self.base_url.rstrip('/')}/identity_/connect/token"
42
+
43
+ data = {
44
+ "grant_type": "client_credentials",
45
+ "client_id": self.client_id,
46
+ "client_secret": self.client_secret,
47
+ "scope": "OrchestratorApiUserAccess",
48
+ }
49
+
50
+ async with httpx.AsyncClient() as client:
51
+ response = await client.post(
52
+ token_endpoint,
53
+ data=data,
54
+ )
55
+ response.raise_for_status()
56
+
57
+ token_data: dict[str, Any] = response.json()
58
+ self.access_token = token_data["access_token"]
59
+
60
+ expires_in = token_data.get("expires_in", 3600)
61
+ self.token_expiry = datetime.now() + timedelta(seconds=expires_in - 60)
62
+
63
+ os.environ["MACHINE_ACCESS_TOKEN"] = self.access_token
64
+
65
+ print(f"[{datetime.now()}] Token acquired successfully")
66
+ return self.access_token
67
+
68
+ def is_token_expired(self) -> bool:
69
+ """Check if the current token is expired or about to expire."""
70
+ if not self.access_token or not self.token_expiry:
71
+ return True
72
+ return datetime.now() >= self.token_expiry
73
+
74
+ async def ensure_valid_token(self) -> str:
75
+ """Ensure we have a valid token, refresh if needed."""
76
+ if self.is_token_expired():
77
+ await self.get_token()
78
+ assert self.access_token is not None
79
+ return self.access_token
@@ -0,0 +1,303 @@
1
+ """Orchestrator service to interact with UiPath Orchestrator APIs."""
2
+
3
+ import base64
4
+ import os
5
+ import platform
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from uipath.robot.infra import get_logger
12
+ from uipath.robot.models import HeartbeatData, HeartbeatResponse
13
+
14
+
15
+ class OrchestratorService:
16
+ """Service for interacting with UiPath Orchestrator APIs."""
17
+
18
+ def __init__(self):
19
+ """Initialize the OrchestratorService with environment variables."""
20
+ self.logger = get_logger()
21
+
22
+ self.access_token = os.getenv("UIPATH_ACCESS_TOKEN")
23
+ self.base_url = os.getenv("UIPATH_URL")
24
+
25
+ if not self.access_token:
26
+ raise ValueError("Missing UIPATH_ACCESS_TOKEN environment variable")
27
+ if not self.base_url:
28
+ raise ValueError("Missing UIPATH_URL environment variable")
29
+
30
+ async def get_shared_connection_data(self) -> str | None:
31
+ """Get shared connection data from Orchestrator.
32
+
33
+ Returns:
34
+ License key string if successful, None otherwise
35
+ """
36
+ assert self.base_url is not None
37
+
38
+ url = f"{self.base_url.rstrip('/')}/orchestrator_/api/robotsservice/getsharedconnectiondata"
39
+
40
+ headers = {
41
+ "Authorization": f"Bearer {self.access_token}",
42
+ "Content-Type": "application/json",
43
+ }
44
+
45
+ async with httpx.AsyncClient() as client:
46
+ try:
47
+ response = await client.get(url, headers=headers)
48
+ response.raise_for_status()
49
+
50
+ data: dict[str, Any] = response.json()
51
+ license_key = data.get("licenseKey")
52
+
53
+ if license_key:
54
+ return license_key
55
+ else:
56
+ self.logger.error("No LicenseKey found in response")
57
+ return None
58
+
59
+ except httpx.HTTPStatusError as e:
60
+ if e.response.status_code == 401:
61
+ self.logger.error("Unauthorized: Invalid or expired token")
62
+ elif e.response.status_code == 409:
63
+ self.logger.error(f"Conflict error occurred: {e.response.text}")
64
+ else:
65
+ self.logger.error(f"HTTP error: {e.response.status_code} - {e}")
66
+ return None
67
+ except httpx.HTTPError as e:
68
+ self.logger.error(f"Request failed: {e}")
69
+ return None
70
+ except Exception as e:
71
+ self.logger.error(f"Unexpected error: {e}")
72
+ return None
73
+
74
+ async def download_package(
75
+ self,
76
+ package_id: str,
77
+ package_version: str,
78
+ output_path: Path,
79
+ feed_id: str | None = None,
80
+ ) -> bool:
81
+ """Download a package from Orchestrator.
82
+
83
+ Args:
84
+ package_id: The package identifier
85
+ package_version: The package version
86
+ output_path: Path where to save the downloaded package
87
+
88
+ Returns:
89
+ True if successful, False otherwise
90
+ """
91
+ assert self.base_url is not None
92
+
93
+ url = f"{self.base_url.rstrip('/')}/orchestrator_/odata/Processes/UiPath.Server.Configuration.OData.DownloadPackage(key='{package_id}:{package_version}')"
94
+
95
+ params = {}
96
+
97
+ if feed_id:
98
+ params["feedId"] = feed_id
99
+
100
+ headers = {
101
+ "Authorization": f"Bearer {self.access_token}",
102
+ }
103
+
104
+ async with httpx.AsyncClient() as client:
105
+ try:
106
+ response = await client.get(url, params=params, headers=headers)
107
+ response.raise_for_status()
108
+
109
+ # Write the package content to file
110
+ output_path.parent.mkdir(parents=True, exist_ok=True)
111
+ with open(output_path, "wb") as f:
112
+ f.write(response.content)
113
+
114
+ return True
115
+
116
+ except httpx.HTTPStatusError as e:
117
+ self.logger.error(
118
+ f"Failed to download package - HTTP {e.response.status_code}: {e}"
119
+ )
120
+ return False
121
+ except httpx.HTTPError as e:
122
+ self.logger.error(f"Failed to download package - Request error: {e}")
123
+ return False
124
+ except Exception as e:
125
+ self.logger.error(f"Failed to download package - Unexpected error: {e}")
126
+ return False
127
+
128
+ async def heartbeat(self, license_key: str) -> HeartbeatResponse:
129
+ """Send heartbeat to Orchestrator.
130
+
131
+ Args:
132
+ license_key: The robot license key. If None, uses stored license_key.
133
+
134
+ Returns:
135
+ HeartbeatResponse object containing commands from Orchestrator
136
+ """
137
+ assert self.base_url is not None
138
+
139
+ url = f"{self.base_url.rstrip('/')}/orchestrator_/api/robotsservice/heartbeatv2"
140
+
141
+ headers = {
142
+ **self._get_robot_headers(license_key),
143
+ }
144
+
145
+ payload = {"CommandState": "All", "Heartbeats": []}
146
+
147
+ async with httpx.AsyncClient() as client:
148
+ try:
149
+ response = await client.post(url, headers=headers, json=payload)
150
+ response.raise_for_status()
151
+ data = response.json()
152
+ return HeartbeatResponse.model_validate(data)
153
+
154
+ except httpx.HTTPStatusError as e:
155
+ self.logger.error(
156
+ f"Heartbeat failed - HTTP {e.response.status_code}: {e}"
157
+ )
158
+ raise e
159
+ except httpx.HTTPError as e:
160
+ self.logger.error(f"Heartbeat failed - Request error: {e}")
161
+ raise e
162
+ except Exception as e:
163
+ self.logger.error(f"Heartbeat failed - Unexpected error: {e}")
164
+ raise e
165
+
166
+ async def start_service(self, license_key: str) -> bool:
167
+ """Send start service request to Orchestrator.
168
+
169
+ Args:
170
+ license_key: The robot license key.
171
+
172
+ Returns:
173
+ True if successful, False otherwise
174
+ """
175
+ assert self.base_url is not None
176
+
177
+ url = (
178
+ f"{self.base_url.rstrip('/')}/orchestrator_/api/robotsservice/startservice"
179
+ )
180
+
181
+ headers = {
182
+ **self._get_robot_headers(license_key),
183
+ }
184
+
185
+ payload: dict[str, Any] = {}
186
+
187
+ async with httpx.AsyncClient() as client:
188
+ try:
189
+ response = await client.post(url, headers=headers, json=payload)
190
+ response.raise_for_status()
191
+ return True
192
+
193
+ except httpx.HTTPStatusError as e:
194
+ self.logger.error(
195
+ f"Start service failed - HTTP {e.response.status_code}: {e}"
196
+ )
197
+ return False
198
+ except httpx.HTTPError as e:
199
+ self.logger.error(f"Start service failed - Request error: {e}")
200
+ return False
201
+ except Exception as e:
202
+ self.logger.error(f"Start service failed - Unexpected error: {e}")
203
+ return False
204
+
205
+ async def stop_service(self, license_key: str) -> bool:
206
+ """Send stop service request to Orchestrator.
207
+
208
+ Args:
209
+ license_key: The robot license key.
210
+
211
+ Returns:
212
+ True if successful, False otherwise
213
+ """
214
+ assert self.base_url is not None
215
+
216
+ url = f"{self.base_url.rstrip('/')}/orchestrator_/api/robotsservice/stopservice"
217
+
218
+ headers = {
219
+ **self._get_robot_headers(license_key),
220
+ }
221
+
222
+ payload: dict[str, Any] = {}
223
+
224
+ async with httpx.AsyncClient() as client:
225
+ try:
226
+ response = await client.post(url, headers=headers, json=payload)
227
+ response.raise_for_status()
228
+ return True
229
+
230
+ except httpx.HTTPStatusError as e:
231
+ self.logger.error(
232
+ f"Stop service failed - HTTP {e.response.status_code}: {e}"
233
+ )
234
+ return False
235
+ except httpx.HTTPError as e:
236
+ self.logger.error(f"Stop service failed - Request error: {e}")
237
+ return False
238
+ except Exception as e:
239
+ self.logger.error(f"Stop service failed - Unexpected error: {e}")
240
+ return False
241
+
242
+ async def submit_job_state(
243
+ self, heartbeats: list[HeartbeatData], license_key: str
244
+ ) -> bool:
245
+ """Submit job states to Orchestrator.
246
+
247
+ Args:
248
+ heartbeats: List of heartbeat data containing job states
249
+ license_key: The robot license key
250
+
251
+ Returns:
252
+ True if successful, False otherwise
253
+ """
254
+ assert self.base_url is not None
255
+
256
+ url = f"{self.base_url.rstrip('/')}/orchestrator_/api/robotsservice/SubmitJobState"
257
+
258
+ headers = {
259
+ **self._get_robot_headers(license_key),
260
+ }
261
+
262
+ payload = [heartbeat.model_dump(by_alias=True) for heartbeat in heartbeats]
263
+
264
+ async with httpx.AsyncClient() as client:
265
+ try:
266
+ response = await client.post(url, headers=headers, json=payload)
267
+ response.raise_for_status()
268
+ return True
269
+
270
+ except httpx.HTTPStatusError as e:
271
+ self.logger.error(
272
+ f"Submit job state failed - HTTP {e.response.status_code}: {e}"
273
+ )
274
+ self.logger.error(f"Response: {e.response.text}")
275
+ return False
276
+ except httpx.HTTPError as e:
277
+ self.logger.error(f"Submit job state failed - Request error: {e}")
278
+ return False
279
+ except Exception as e:
280
+ self.logger.error(f"Submit job state failed - Unexpected error: {e}")
281
+ return False
282
+
283
+ def _get_robot_headers(self, license_key: str) -> dict[str, str]:
284
+ """Get robot-specific headers for heartbeat calls.
285
+
286
+ Args:
287
+ license_key: The robot license key
288
+
289
+ Returns:
290
+ Dictionary of robot headers
291
+ """
292
+ machine = platform.node()
293
+ hostname = f"{machine}-py"
294
+
295
+ return {
296
+ "X-ROBOT-VERSION": "24.10",
297
+ "X-ROBOT-MACHINE": hostname,
298
+ "X-ROBOT-MACHINE-ENCODED": base64.b64encode(
299
+ hostname.encode("utf-8")
300
+ ).decode("utf-8"),
301
+ "X-ROBOT-LICENSE": license_key,
302
+ "X-ROBOT-AGENT": "OS=Windows",
303
+ }
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: uipath-robot
3
+ Version: 0.0.3
4
+ Summary: UiPath Robot Simulator
5
+ Project-URL: Homepage, https://uipath.com
6
+ Project-URL: Documentation, https://uipath.github.io/uipath-python/
7
+ Maintainer-email: Cristian Pufu <cristian.pufu@uipath.com>
8
+ License-File: LICENSE
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Software Development :: Build Tools
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: httpx>=0.28.1
16
+ Requires-Dist: pydantic<3.0.0,>=2.12.5
17
+ Requires-Dist: uipath>=2.4.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # UiPath Robot
21
+
22
+ [![PyPI downloads](https://img.shields.io/pypi/dm/uipath-robot.svg)](https://pypi.org/project/uipath-robot/)
23
+ [![PyPI - Version](https://img.shields.io/pypi/v/uipath-robot)](https://img.shields.io/pypi/v/uipath-robot)
24
+ [![Python versions](https://img.shields.io/pypi/pyversions/uipath-robot.svg)](https://pypi.org/project/uipath-robot/)
25
+
26
+ Robot simulator
@@ -0,0 +1,14 @@
1
+ uipath/robot/__init__.py,sha256=gepn5ptexouVNXPLg57lUh3hZk_mNKJigt228mn09Wg,17486
2
+ uipath/robot/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ uipath/robot/infra/__init__.py,sha256=r3uLvwBRZ9KiAjKPv71bAAdCMXA2yD7TcJuYPir4n7s,147
4
+ uipath/robot/infra/logger.py,sha256=rrHINv5zn18Cu41OAf1sSRPNisEmlST_zBDfmDnRr2s,5654
5
+ uipath/robot/models/__init__.py,sha256=MrFMohE9xtbXZTYyvvUm2vKXhT46M1ykHciIl_xFyGI,345
6
+ uipath/robot/models/heartbeat.py,sha256=_9SMyMfgJQYOWAZfnOObrPLwF4_zxZO0IPkM3NAzRhw,5677
7
+ uipath/robot/services/__init__.py,sha256=zLA_yMsrXTexcCE_lIa3TkMb9aN-LUgHXxL1ddPR58w,175
8
+ uipath/robot/services/identity.py,sha256=1PGrSBxtCyKhET8wpSl_-b3b2zkskYRTiV_NpDjnUnU,2774
9
+ uipath/robot/services/orchestrator.py,sha256=obX2hFzmwtBVemX_k1nD4c64HaZNausXeWrNRFhBU3g,10342
10
+ uipath_robot-0.0.3.dist-info/METADATA,sha256=oFOP7rCwsfehrgancc8szE2YOlVJlhU_4bhCzjcyjrU,1063
11
+ uipath_robot-0.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ uipath_robot-0.0.3.dist-info/entry_points.txt,sha256=AvnQfA63XXwIbb4EuWYd_J2Iqc2zwX3MGNglfUIxMpw,60
13
+ uipath_robot-0.0.3.dist-info/licenses/LICENSE,sha256=-KBavWXepyDjimmzH5fVAsi-6jNVpIKFc2kZs0Ri4ng,1058
14
+ uipath_robot-0.0.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ uipath-robot = uipath.robot.__init__:main
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright 2025 UiPath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.