orgo 0.0.35__py3-none-any.whl → 0.0.38__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.
orgo/computer.py CHANGED
@@ -1,9 +1,19 @@
1
- """Computer class for interacting with Orgo virtual environments"""
1
+ """
2
+ Orgo Computer - Control virtual computers with AI.
3
+
4
+ Usage:
5
+ from orgo import Computer
6
+
7
+ computer = Computer(project="your-project")
8
+ computer.prompt("Open Firefox and search for AI news")
9
+ """
10
+
2
11
  import os as operating_system
3
12
  import base64
4
13
  import logging
5
14
  import uuid
6
15
  import io
16
+ import random
7
17
  from typing import Dict, List, Any, Optional, Callable, Literal, Union
8
18
  from PIL import Image
9
19
  import requests
@@ -14,7 +24,48 @@ from .prompt import get_provider
14
24
 
15
25
  logger = logging.getLogger(__name__)
16
26
 
27
+
28
+ def _generate_computer_name() -> str:
29
+ """Generate a random computer name like 'computer-1568'"""
30
+ return f"computer-{random.randint(1000, 9999)}"
31
+
32
+
33
+ def _print_success(message: str):
34
+ """Print a success message with nice formatting"""
35
+ print(f"✓ {message}")
36
+
37
+
38
+ def _print_error(message: str):
39
+ """Print an error message with nice formatting"""
40
+ print(f"✗ {message}")
41
+
42
+
43
+ def _print_info(message: str):
44
+ """Print an info message with nice formatting"""
45
+ print(f"→ {message}")
46
+
47
+
17
48
  class Computer:
49
+ """
50
+ Control an Orgo virtual computer.
51
+
52
+ Examples:
53
+ # Create computer in new/existing project
54
+ computer = Computer(project="my-project")
55
+
56
+ # Create with specific name
57
+ computer = Computer(project="my-project", name="dev-machine")
58
+
59
+ # Connect to existing computer by ID
60
+ computer = Computer(computer_id="abc123")
61
+
62
+ # AI control (uses Orgo by default)
63
+ computer.prompt("Open Firefox")
64
+
65
+ # AI control with Anthropic directly
66
+ computer.prompt("Open Firefox", provider="anthropic")
67
+ """
68
+
18
69
  def __init__(self,
19
70
  project: Optional[Union[str, 'Project']] = None,
20
71
  name: Optional[str] = None,
@@ -26,251 +77,252 @@ class Computer:
26
77
  cpu: Optional[Literal[1, 2, 4, 8, 16]] = None,
27
78
  os: Optional[Literal["linux", "windows"]] = None,
28
79
  gpu: Optional[Literal["none", "a10", "l40s", "a100-40gb", "a100-80gb"]] = None,
29
- image: Optional[Union[str, Any]] = None):
80
+ image: Optional[Union[str, Any]] = None,
81
+ verbose: bool = True):
30
82
  """
31
83
  Initialize an Orgo virtual computer.
32
84
 
33
85
  Args:
34
- project: Project name (str) or Project instance. If not provided, creates a new project.
35
- name: Computer name within the project (optional, auto-generated if not provided)
36
- computer_id: Existing computer ID to connect to (optional)
86
+ project: Project name or Project instance (creates if doesn't exist)
87
+ name: Computer name (auto-generated if not provided)
88
+ computer_id: Connect to existing computer by ID
37
89
  api_key: Orgo API key (defaults to ORGO_API_KEY env var)
38
- base_api_url: Custom API URL (optional)
39
- ram/memory: RAM in GB (1, 2, 4, 8, 16, 32, or 64) - only used when creating
40
- cpu: CPU cores (1, 2, 4, 8, or 16) - only used when creating
41
- os: Operating system ("linux" or "windows") - only used when creating
42
- gpu: GPU type - only used when creating
43
- image: Custom image reference (str) or Forge object - only used when creating
44
-
45
- Examples:
46
- # Create computer in new project
47
- computer = Computer(ram=4, cpu=2)
48
-
49
- # Create computer with custom image
50
- forge = Forge(org_id="myorg", project_id="myproj").base("ubuntu").run("echo hello")
51
- computer = Computer(image=forge)
52
-
53
- # Create computer in existing project
54
- computer = Computer(project="manus", ram=4, cpu=2)
55
-
56
- # Connect to existing computer by ID
57
- computer = Computer(computer_id="11c4fd46-e069-4c32-be65-f82d9f87b9b8")
90
+ base_api_url: Custom API URL
91
+ ram/memory: RAM in GB (1, 2, 4, 8, 16, 32, 64)
92
+ cpu: CPU cores (1, 2, 4, 8, 16)
93
+ os: "linux" or "windows"
94
+ gpu: "none", "a10", "l40s", "a100-40gb", "a100-80gb"
95
+ image: Custom image reference or Forge object
96
+ verbose: Show console output (default: True)
58
97
  """
59
98
  self.api_key = api_key or operating_system.environ.get("ORGO_API_KEY")
60
99
  self.base_api_url = base_api_url
61
100
  self.api = ApiClient(self.api_key, self.base_api_url)
101
+ self.verbose = verbose
62
102
 
63
- # Handle memory parameter as an alias for ram
64
103
  if ram is None and memory is not None:
65
104
  ram = memory
66
105
 
67
- # Store configuration
68
106
  self.os = os or "linux"
69
107
  self.ram = ram or 2
70
108
  self.cpu = cpu or 2
71
109
  self.gpu = gpu or "none"
72
-
73
- # Handle image
74
110
  self.image = image
111
+
75
112
  if hasattr(self.image, 'build') and callable(self.image.build):
76
- logger.info("Building image from Forge object...")
113
+ if self.verbose:
114
+ _print_info("Building image from Forge object...")
77
115
  self.image = self.image.build()
78
116
 
79
117
  if computer_id:
80
-
81
- # Just store the computer ID, no API call needed
82
118
  self.computer_id = computer_id
83
119
  self.name = name
84
120
  self.project_id = None
85
121
  self.project_name = None
86
- logger.info(f"Connected to computer ID: {self.computer_id}")
122
+ if self.verbose:
123
+ _print_success(f"Connected to computer: {self.computer_id}")
87
124
  elif project:
88
- # Work with specified project
89
125
  if isinstance(project, str):
90
- # Project name provided
91
126
  self.project_name = project
92
127
  self._initialize_with_project_name(project, name)
93
128
  else:
94
- # Project instance provided
95
129
  from .project import Project as ProjectClass
96
130
  if isinstance(project, ProjectClass):
97
131
  self.project_name = project.name
98
132
  self.project_id = project.id
99
133
  self._initialize_with_project_instance(project, name)
100
134
  else:
101
- raise ValueError("project must be a string (project name) or Project instance")
135
+ raise ValueError("project must be a string or Project instance")
102
136
  else:
103
- # No project specified, create a new one
104
137
  self._create_new_project_and_computer(name)
105
138
 
139
+ # =========================================================================
140
+ # Initialization Helpers
141
+ # =========================================================================
142
+
106
143
  def _initialize_with_project_name(self, project_name: str, computer_name: Optional[str]):
107
- """Initialize with a project name (create project if needed)"""
144
+ """Initialize computer with project name (create project if needed)"""
108
145
  try:
109
146
  # Try to get existing project
110
147
  project = self.api.get_project_by_name(project_name)
111
148
  self.project_id = project.get("id")
112
149
 
113
- # Check for existing computers
114
- computers = self.api.list_computers(self.project_id)
150
+ # If no computer name specified, generate one
151
+ if not computer_name:
152
+ computer_name = _generate_computer_name()
153
+
154
+ # Create the computer in this project
155
+ self._create_computer(self.project_id, computer_name, project_name)
115
156
 
116
- if computer_name:
117
- # Look for specific computer
118
- existing = next((c for c in computers if c.get("name") == computer_name), None)
119
- if existing:
120
- self._connect_to_existing_computer(existing)
121
- else:
122
- # Create new computer with specified name
123
- self._create_computer(self.project_id, computer_name)
124
- elif computers:
125
- # No name specified, use first available computer
126
- self._connect_to_existing_computer(computers[0])
127
- else:
128
- # No computers exist, create new one
129
- self._create_computer(self.project_id, computer_name)
130
-
131
157
  except Exception:
132
158
  # Project doesn't exist, create it
133
- logger.info(f"Project {project_name} not found, creating new project")
159
+ if self.verbose:
160
+ _print_info(f"Creating project: {project_name}")
134
161
  project = self.api.create_project(project_name)
135
162
  self.project_id = project.get("id")
136
- self._create_computer(self.project_id, computer_name)
163
+
164
+ # Generate name if not specified
165
+ if not computer_name:
166
+ computer_name = _generate_computer_name()
167
+
168
+ self._create_computer(self.project_id, computer_name, project_name)
137
169
 
138
170
  def _initialize_with_project_instance(self, project: 'Project', computer_name: Optional[str]):
139
- """Initialize with a Project instance"""
140
- computers = project.list_computers()
171
+ """Initialize computer with Project instance"""
172
+ # Generate name if not specified
173
+ if not computer_name:
174
+ computer_name = _generate_computer_name()
141
175
 
142
- if computer_name:
143
- # Look for specific computer
144
- existing = next((c for c in computers if c.get("name") == computer_name), None)
145
- if existing:
146
- self._connect_to_existing_computer(existing)
147
- else:
148
- # Create new computer with specified name
149
- self._create_computer(project.id, computer_name)
150
- elif computers:
151
- # No name specified, use first available computer
152
- self._connect_to_existing_computer(computers[0])
153
- else:
154
- # No computers exist, create new one
155
- self._create_computer(project.id, computer_name)
176
+ self._create_computer(project.id, computer_name, project.name)
156
177
 
157
178
  def _create_new_project_and_computer(self, computer_name: Optional[str]):
158
- """Create a new project and computer"""
159
- # Generate a unique project name
179
+ """Create a new project and computer when no project specified"""
160
180
  project_name = f"project-{uuid.uuid4().hex[:8]}"
161
181
 
162
- # Create the project
182
+ if self.verbose:
183
+ _print_info(f"Creating project: {project_name}")
184
+
163
185
  project = self.api.create_project(project_name)
164
186
  self.project_id = project.get("id")
165
187
  self.project_name = project_name
166
188
 
167
- # Create a computer in the new project
168
- self._create_computer(self.project_id, computer_name)
189
+ # Generate name if not specified
190
+ if not computer_name:
191
+ computer_name = _generate_computer_name()
192
+
193
+ self._create_computer(self.project_id, computer_name, project_name)
169
194
 
170
195
  def _connect_to_existing_computer(self, computer_info: Dict[str, Any]):
171
196
  """Connect to an existing computer"""
172
197
  self.computer_id = computer_info.get("id")
173
198
  self.name = computer_info.get("name")
174
- logger.info(f"Connected to existing computer {self.name} (ID: {self.computer_id})")
199
+ if self.verbose:
200
+ _print_success(f"Connected to: {self.name} ({self.computer_id})")
175
201
 
176
- def _create_computer(self, project_id: str, computer_name: Optional[str]):
177
- """Create a new computer in the project"""
178
- # Generate name if not provided
179
- if not computer_name:
180
- computer_name = f"desktop-{uuid.uuid4().hex[:8]}"
181
-
202
+ def _create_computer(self, project_id: str, computer_name: str, project_name: str):
203
+ """Create a new computer with beautiful console output"""
182
204
  self.name = computer_name
183
205
 
184
206
  # Validate parameters
185
207
  if self.ram not in [1, 2, 4, 8, 16, 32, 64]:
186
- raise ValueError("ram must be one of: 1, 2, 4, 8, 16, 32, 64 GB")
208
+ raise ValueError("ram must be: 1, 2, 4, 8, 16, 32, or 64 GB")
187
209
  if self.cpu not in [1, 2, 4, 8, 16]:
188
- raise ValueError("cpu must be one of: 1, 2, 4, 8, 16 cores")
210
+ raise ValueError("cpu must be: 1, 2, 4, 8, or 16 cores")
189
211
  if self.os not in ["linux", "windows"]:
190
- raise ValueError("os must be either 'linux' or 'windows'")
212
+ raise ValueError("os must be: 'linux' or 'windows'")
191
213
  if self.gpu not in ["none", "a10", "l40s", "a100-40gb", "a100-80gb"]:
192
- raise ValueError("gpu must be one of: 'none', 'a10', 'l40s', 'a100-40gb', 'a100-80gb'")
193
-
194
- # Resolve image name if needed
214
+ raise ValueError("gpu must be: 'none', 'a10', 'l40s', 'a100-40gb', or 'a100-80gb'")
215
+
216
+ # Resolve image if needed
195
217
  image_ref = self.image
196
218
  if image_ref and isinstance(image_ref, str) and not image_ref.startswith("registry.fly.io"):
197
- logger.info(f"Resolving image name '{image_ref}'...")
198
219
  try:
199
- # Try to get org_id from project info
200
220
  project_info = self.api.get_project(project_id)
201
- org_id = project_info.get("org_id", "orgo") # Default to 'orgo'
202
-
221
+ org_id = project_info.get("org_id", "orgo")
203
222
  response = self.api.get_latest_build(org_id, project_id, image_ref)
204
223
  if response and response.get("build"):
205
- resolved_ref = response.get("build", {}).get("imageRef")
206
- if resolved_ref:
207
- logger.info(f"Resolved '{image_ref}' to '{resolved_ref}'")
208
- image_ref = resolved_ref
209
- else:
210
- logger.warning(f"Build found for '{image_ref}' but no imageRef present.")
211
- else:
212
- logger.warning(f"Could not resolve image name '{self.image}'. Using as is.")
224
+ resolved = response.get("build", {}).get("imageRef")
225
+ if resolved:
226
+ image_ref = resolved
213
227
  except Exception as e:
214
- logger.warning(f"Failed to resolve image name: {e}")
228
+ if self.verbose:
229
+ logger.warning(f"Failed to resolve image: {e}")
215
230
 
216
- computer = self.api.create_computer(
217
- project_id=project_id,
218
- computer_name=computer_name,
219
- os=self.os,
220
- ram=self.ram,
221
- cpu=self.cpu,
222
- gpu=self.gpu,
223
- image=image_ref
224
- )
225
- self.computer_id = computer.get("id")
226
- logger.info(f"Created new computer {self.name} (ID: {self.computer_id})")
231
+ # Create the computer
232
+ try:
233
+ computer = self.api.create_computer(
234
+ project_id=project_id,
235
+ computer_name=computer_name,
236
+ os=self.os,
237
+ ram=self.ram,
238
+ cpu=self.cpu,
239
+ gpu=self.gpu,
240
+ image=image_ref
241
+ )
242
+ self.computer_id = computer.get("id")
243
+
244
+ # Beautiful success message
245
+ if self.verbose:
246
+ _print_success(
247
+ f"Computer [{self.name}] successfully created under workspace [{project_name}]"
248
+ )
249
+ _print_info(f"ID: {self.computer_id}")
250
+ _print_info(f"View at: https://orgo.ai/workspaces/{self.computer_id}")
251
+
252
+ except Exception as e:
253
+ if self.verbose:
254
+ _print_error(f"Failed to create computer: {str(e)}")
255
+ raise
256
+
257
+ # =========================================================================
258
+ # Computer Management
259
+ # =========================================================================
227
260
 
228
261
  def status(self) -> Dict[str, Any]:
229
- """Get current computer status"""
262
+ """Get current computer status."""
230
263
  return self.api.get_computer(self.computer_id)
231
264
 
232
265
  def restart(self) -> Dict[str, Any]:
233
- """Restart the computer"""
234
- return self.api.restart_computer(self.computer_id)
266
+ """Restart the computer."""
267
+ if self.verbose:
268
+ _print_info(f"Restarting computer: {self.name}")
269
+ result = self.api.restart_computer(self.computer_id)
270
+ if self.verbose:
271
+ _print_success("Computer restarted")
272
+ return result
235
273
 
236
274
  def destroy(self) -> Dict[str, Any]:
237
- """Terminate and delete the computer instance"""
238
- return self.api.delete_computer(self.computer_id)
275
+ """Delete the computer."""
276
+ if self.verbose:
277
+ _print_info(f"Deleting computer: {self.name}")
278
+ result = self.api.delete_computer(self.computer_id)
279
+ if self.verbose:
280
+ _print_success("Computer deleted")
281
+ return result
282
+
283
+ # =========================================================================
284
+ # Mouse Actions
285
+ # =========================================================================
239
286
 
240
- # Navigation methods
241
287
  def left_click(self, x: int, y: int) -> Dict[str, Any]:
242
- """Perform left mouse click at specified coordinates"""
288
+ """Left click at coordinates."""
243
289
  return self.api.left_click(self.computer_id, x, y)
244
290
 
245
291
  def right_click(self, x: int, y: int) -> Dict[str, Any]:
246
- """Perform right mouse click at specified coordinates"""
292
+ """Right click at coordinates."""
247
293
  return self.api.right_click(self.computer_id, x, y)
248
294
 
249
295
  def double_click(self, x: int, y: int) -> Dict[str, Any]:
250
- """Perform double click at specified coordinates"""
296
+ """Double click at coordinates."""
251
297
  return self.api.double_click(self.computer_id, x, y)
252
298
 
253
299
  def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
254
300
  button: str = "left", duration: float = 0.5) -> Dict[str, Any]:
255
- """Perform a smooth drag operation from start to end coordinates"""
301
+ """Drag from start to end coordinates."""
256
302
  return self.api.drag(self.computer_id, start_x, start_y, end_x, end_y, button, duration)
257
303
 
258
304
  def scroll(self, direction: str = "down", amount: int = 3) -> Dict[str, Any]:
259
- """Scroll in specified direction and amount"""
305
+ """Scroll in direction."""
260
306
  return self.api.scroll(self.computer_id, direction, amount)
261
307
 
262
- # Input methods
308
+ # =========================================================================
309
+ # Keyboard Actions
310
+ # =========================================================================
311
+
263
312
  def type(self, text: str) -> Dict[str, Any]:
264
- """Type the specified text"""
313
+ """Type text."""
265
314
  return self.api.type_text(self.computer_id, text)
266
315
 
267
316
  def key(self, key: str) -> Dict[str, Any]:
268
- """Press a key or key combination (e.g., "Enter", "ctrl+c")"""
317
+ """Press key (e.g., "Enter", "ctrl+c")."""
269
318
  return self.api.key_press(self.computer_id, key)
270
319
 
271
- # View methods
320
+ # =========================================================================
321
+ # Screen Capture
322
+ # =========================================================================
323
+
272
324
  def screenshot(self) -> Image.Image:
273
- """Capture screenshot and return as PIL Image"""
325
+ """Capture screenshot as PIL Image."""
274
326
  response = self.api.get_screenshot(self.computer_id)
275
327
  image_data = response.get("image", "")
276
328
 
@@ -279,11 +331,10 @@ class Computer:
279
331
  img_response.raise_for_status()
280
332
  return Image.open(io.BytesIO(img_response.content))
281
333
  else:
282
- img_data = base64.b64decode(image_data)
283
- return Image.open(io.BytesIO(img_data))
334
+ return Image.open(io.BytesIO(base64.b64decode(image_data)))
284
335
 
285
336
  def screenshot_base64(self) -> str:
286
- """Capture screenshot and return as base64 string"""
337
+ """Capture screenshot as base64 string."""
287
338
  response = self.api.get_screenshot(self.computer_id)
288
339
  image_data = response.get("image", "")
289
340
 
@@ -291,58 +342,102 @@ class Computer:
291
342
  img_response = requests.get(image_data)
292
343
  img_response.raise_for_status()
293
344
  return base64.b64encode(img_response.content).decode('utf-8')
294
- else:
295
- return image_data
345
+ return image_data
346
+
347
+ # =========================================================================
348
+ # Code Execution
349
+ # =========================================================================
296
350
 
297
- # Execution methods
298
351
  def bash(self, command: str) -> str:
299
- """Execute a bash command and return output"""
352
+ """Execute bash command."""
300
353
  response = self.api.execute_bash(self.computer_id, command)
301
354
  return response.get("output", "")
302
355
 
303
356
  def exec(self, code: str, timeout: int = 10) -> Dict[str, Any]:
304
- """Execute Python code on the remote computer"""
305
- response = self.api.execute_python(self.computer_id, code, timeout)
306
- return response
357
+ """Execute Python code."""
358
+ return self.api.execute_python(self.computer_id, code, timeout)
307
359
 
308
360
  def wait(self, seconds: float) -> Dict[str, Any]:
309
- """Wait for specified number of seconds"""
361
+ """Wait for seconds."""
310
362
  return self.api.wait(self.computer_id, seconds)
311
363
 
312
- # Streaming methods
364
+ # =========================================================================
365
+ # Streaming
366
+ # =========================================================================
367
+
313
368
  def start_stream(self, connection: str) -> Dict[str, Any]:
314
- """Start streaming the computer screen to an RTMP server"""
369
+ """Start RTMP stream."""
315
370
  return self.api.start_stream(self.computer_id, connection)
316
371
 
317
372
  def stop_stream(self) -> Dict[str, Any]:
318
- """Stop the active stream"""
373
+ """Stop stream."""
319
374
  return self.api.stop_stream(self.computer_id)
320
375
 
321
376
  def stream_status(self) -> Dict[str, Any]:
322
- """Get the current streaming status"""
377
+ """Get stream status."""
323
378
  return self.api.get_stream_status(self.computer_id)
324
379
 
325
- # AI control method
380
+ # =========================================================================
381
+ # AI Control
382
+ # =========================================================================
383
+
326
384
  def prompt(self,
327
385
  instruction: str,
328
- provider: str = "anthropic",
386
+ provider: Optional[str] = None,
387
+ verbose: bool = True,
388
+ callback: Optional[Callable[[str, Any], None]] = None,
329
389
  model: str = "claude-sonnet-4-5-20250929",
330
390
  display_width: int = 1024,
331
391
  display_height: int = 768,
332
- callback: Optional[Callable[[str, Any], None]] = None,
333
392
  thinking_enabled: bool = True,
334
393
  thinking_budget: int = 1024,
335
394
  max_tokens: int = 4096,
336
395
  max_iterations: int = 100,
337
396
  max_saved_screenshots: int = 3,
397
+ system_prompt: Optional[str] = None,
338
398
  api_key: Optional[str] = None) -> List[Dict[str, Any]]:
339
- """Control the computer with natural language instructions using an AI assistant"""
399
+ """
400
+ Control the computer with natural language.
401
+
402
+ Args:
403
+ instruction: What you want the computer to do
404
+ provider: "orgo" (default) or "anthropic"
405
+ verbose: Show progress logs (default: True)
406
+ callback: Optional callback for events
407
+ model: AI model to use
408
+ display_width: Screen width
409
+ display_height: Screen height
410
+ thinking_enabled: Enable extended thinking
411
+ thinking_budget: Token budget for thinking
412
+ max_tokens: Max response tokens
413
+ max_iterations: Max agent iterations
414
+ max_saved_screenshots: Screenshots to keep in context
415
+ system_prompt: Custom instructions
416
+ api_key: Anthropic key (only for provider="anthropic")
417
+
418
+ Returns:
419
+ List of conversation messages
420
+
421
+ Examples:
422
+ # Default: Uses Orgo hosted agent
423
+ computer.prompt("Open Firefox and search for AI news")
424
+
425
+ # Quiet mode (no logs)
426
+ computer.prompt("Open Firefox", verbose=False)
427
+
428
+ # Use Anthropic directly
429
+ computer.prompt("Open Firefox", provider="anthropic")
430
+
431
+ # With callback
432
+ computer.prompt("Search Google", callback=lambda t, d: print(f"{t}: {d}"))
433
+ """
340
434
  provider_instance = get_provider(provider)
341
435
 
342
436
  return provider_instance.execute(
343
437
  computer_id=self.computer_id,
344
438
  instruction=instruction,
345
439
  callback=callback,
440
+ verbose=verbose,
346
441
  api_key=api_key,
347
442
  model=model,
348
443
  display_width=display_width,
@@ -352,11 +447,21 @@ class Computer:
352
447
  max_tokens=max_tokens,
353
448
  max_iterations=max_iterations,
354
449
  max_saved_screenshots=max_saved_screenshots,
450
+ system_prompt=system_prompt,
355
451
  orgo_api_key=self.api_key,
356
452
  orgo_base_url=self.base_api_url
357
453
  )
358
454
 
455
+ # =========================================================================
456
+ # URL Helper
457
+ # =========================================================================
458
+
459
+ @property
460
+ def url(self) -> str:
461
+ """Get the URL to view this computer."""
462
+ return f"https://orgo.ai/workspaces/{self.computer_id}"
463
+
359
464
  def __repr__(self):
360
- project_str = f", project='{self.project_name}'" if hasattr(self, 'project_name') and self.project_name else ""
361
- name_str = f"name='{self.name}'" if hasattr(self, 'name') and self.name else f"id='{self.computer_id}'"
362
- return f"Computer({name_str}{project_str})"
465
+ if hasattr(self, 'name') and self.name:
466
+ return f"Computer(name='{self.name}', id='{self.computer_id}')"
467
+ return f"Computer(id='{self.computer_id}')"