weco 0.3.7__py3-none-any.whl → 0.3.9__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.
weco/api.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import sys
2
+ from dataclasses import dataclass
2
3
  from typing import Dict, Any, Optional, Union, Tuple
3
4
  import requests
4
5
  from rich.console import Console
@@ -7,6 +8,24 @@ from weco import __pkg_version__, __base_url__
7
8
  from .utils import truncate_output
8
9
 
9
10
 
11
+ @dataclass
12
+ class RunSummary:
13
+ """Brief run summary from execution task response."""
14
+
15
+ id: str
16
+ status: str
17
+ name: Optional[str] = None
18
+ require_review: bool = False
19
+
20
+
21
+ @dataclass
22
+ class ExecutionTasksResult:
23
+ """Result from get_execution_tasks containing tasks and run info."""
24
+
25
+ tasks: list
26
+ run: Optional[RunSummary] = None
27
+
28
+
10
29
  def handle_api_error(e: requests.exceptions.HTTPError, console: Console) -> None:
11
30
  """Extract and display error messages from API responses in a structured format."""
12
31
  status = getattr(e.response, "status_code", None)
@@ -110,6 +129,7 @@ def start_optimization_run(
110
129
  auth_headers: dict = {},
111
130
  timeout: Union[int, Tuple[int, int]] = (10, 3650),
112
131
  api_keys: Optional[Dict[str, str]] = None,
132
+ require_review: bool = False,
113
133
  ) -> Optional[Dict[str, Any]]:
114
134
  """Start the optimization run."""
115
135
  with console.status("[bold green]Starting Optimization..."):
@@ -128,6 +148,7 @@ def start_optimization_run(
128
148
  "eval_timeout": eval_timeout,
129
149
  "save_logs": save_logs,
130
150
  "log_dir": log_dir,
151
+ "require_review": require_review,
131
152
  "metadata": {"client_name": "cli", "client_version": __pkg_version__},
132
153
  }
133
154
  if api_keys:
@@ -315,3 +336,107 @@ def report_termination(
315
336
  except Exception as e:
316
337
  print(f"Warning: Failed to report termination to backend for run {run_id}: {e}", file=sys.stderr)
317
338
  return False
339
+
340
+
341
+ # --- Execution Queue API Functions ---
342
+
343
+
344
+ def get_execution_tasks(
345
+ run_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = (5, 30)
346
+ ) -> Optional[ExecutionTasksResult]:
347
+ """Poll for ready execution tasks.
348
+
349
+ Args:
350
+ run_id: The run ID to get tasks for.
351
+ auth_headers: Authentication headers.
352
+ timeout: Request timeout.
353
+
354
+ Returns:
355
+ ExecutionTasksResult with tasks and run summary, or None if request failed.
356
+ """
357
+ try:
358
+ response = requests.get(
359
+ f"{__base_url__}/execution-tasks/", params={"run_id": run_id}, headers=auth_headers, timeout=timeout
360
+ )
361
+ response.raise_for_status()
362
+ data = response.json()
363
+
364
+ # Extract run summary from top-level run field
365
+ run_summary = None
366
+ if data.get("run"):
367
+ run_data = data["run"]
368
+ run_summary = RunSummary(
369
+ id=run_data["id"],
370
+ status=run_data["status"],
371
+ name=run_data.get("name"),
372
+ require_review=run_data.get("require_review", False),
373
+ )
374
+
375
+ return ExecutionTasksResult(tasks=data.get("tasks", []), run=run_summary)
376
+ except requests.exceptions.HTTPError:
377
+ return None
378
+ except Exception:
379
+ return None
380
+
381
+
382
+ def claim_execution_task(
383
+ task_id: str, auth_headers: dict = {}, timeout: Union[int, Tuple[int, int]] = (5, 30)
384
+ ) -> Optional[Dict[str, Any]]:
385
+ """Claim an execution task.
386
+
387
+ Args:
388
+ task_id: The task ID to claim.
389
+ auth_headers: Authentication headers.
390
+ timeout: Request timeout.
391
+
392
+ Returns:
393
+ The claimed task with revision, or None if already claimed or error.
394
+ """
395
+ try:
396
+ response = requests.post(f"{__base_url__}/execution-tasks/{task_id}/claim", headers=auth_headers, timeout=timeout)
397
+ if response.status_code == 409:
398
+ return None # Already claimed
399
+ response.raise_for_status()
400
+ return response.json()
401
+ except requests.exceptions.HTTPError:
402
+ return None
403
+ except Exception:
404
+ return None
405
+
406
+
407
+ def submit_execution_result(
408
+ run_id: str,
409
+ task_id: str,
410
+ execution_output: str,
411
+ auth_headers: dict = {},
412
+ timeout: Union[int, Tuple[int, int]] = (10, 3650),
413
+ api_keys: Optional[Dict[str, str]] = None,
414
+ ) -> Optional[Dict[str, Any]]:
415
+ """Submit execution result for a task.
416
+
417
+ Args:
418
+ run_id: The run ID.
419
+ task_id: The task ID being completed.
420
+ execution_output: The execution output to submit.
421
+ auth_headers: Authentication headers.
422
+ timeout: Request timeout.
423
+ api_keys: Optional API keys for LLM providers.
424
+
425
+ Returns:
426
+ The suggest response, or None if request failed.
427
+ """
428
+ try:
429
+ truncated_output = truncate_output(execution_output)
430
+ request_json = {"execution_output": truncated_output, "task_id": task_id, "metadata": {}}
431
+ if api_keys:
432
+ request_json["api_keys"] = api_keys
433
+
434
+ response = requests.post(
435
+ f"{__base_url__}/runs/{run_id}/suggest", json=request_json, headers=auth_headers, timeout=timeout
436
+ )
437
+ response.raise_for_status()
438
+ return response.json()
439
+ except requests.exceptions.HTTPError:
440
+ return None
441
+ except Exception:
442
+ return None
weco/browser.py ADDED
@@ -0,0 +1,29 @@
1
+ """Cross-platform browser utilities."""
2
+
3
+ import webbrowser
4
+
5
+
6
+ def open_browser(url: str) -> bool:
7
+ """
8
+ Open a URL in the user's default web browser.
9
+
10
+ This function is cross-platform compatible (Windows, macOS, Linux).
11
+ It uses Python's built-in webbrowser module which handles platform
12
+ detection automatically.
13
+
14
+ Args:
15
+ url: The URL to open in the browser.
16
+
17
+ Returns:
18
+ True if the browser was opened successfully, False otherwise.
19
+ """
20
+ try:
21
+ # webbrowser.open() is cross-platform and uses the default browser
22
+ # - On macOS: uses 'open' command
23
+ # - On Windows: uses 'start' command
24
+ # - On Linux: tries common browsers (xdg-open, gnome-open, etc.)
25
+ return webbrowser.open(url)
26
+ except Exception:
27
+ # Silently fail - browser opening is a convenience feature
28
+ # and should not interrupt the optimization flow
29
+ return False
weco/cli.py CHANGED
@@ -3,9 +3,10 @@ import sys
3
3
  from rich.console import Console
4
4
  from rich.traceback import install
5
5
 
6
- from .auth import clear_api_key
6
+ from .auth import clear_api_key, perform_login, load_weco_api_key
7
7
  from .constants import DEFAULT_MODELS
8
8
  from .utils import check_for_cli_updates, get_default_model, UnrecognizedAPIKeysError, DefaultModelNotFoundError
9
+ from .validation import validate_source_file, validate_log_directory, ValidationError, print_validation_error
9
10
 
10
11
 
11
12
  install(show_locals=True)
@@ -108,6 +109,11 @@ def configure_run_parser(run_parser: argparse.ArgumentParser) -> None:
108
109
  action="store_true",
109
110
  help="Automatically apply the best solution to the source file without prompting",
110
111
  )
112
+ run_parser.add_argument(
113
+ "--require-review",
114
+ action="store_true",
115
+ help="Require manual review and approval of each proposed change before execution",
116
+ )
111
117
 
112
118
  default_api_keys = " ".join([f"{provider}=xxx" for provider, _ in DEFAULT_MODELS])
113
119
  supported_providers = ", ".join([provider for provider, _ in DEFAULT_MODELS])
@@ -131,6 +137,13 @@ Default models for providers:
131
137
  {default_models_for_providers}
132
138
  """,
133
139
  )
140
+ run_parser.add_argument(
141
+ "--output",
142
+ type=str,
143
+ choices=["rich", "plain"],
144
+ default="rich",
145
+ help="Output mode: 'rich' for interactive terminal UI (default), 'plain' for machine-readable text output suitable for LLM agents.",
146
+ )
134
147
 
135
148
 
136
149
  def configure_credits_parser(credits_parser: argparse.ArgumentParser) -> None:
@@ -172,6 +185,12 @@ def configure_credits_parser(credits_parser: argparse.ArgumentParser) -> None:
172
185
  )
173
186
 
174
187
 
188
+ def configure_setup_parser(setup_parser: argparse.ArgumentParser) -> None:
189
+ """Configure the setup command parser and its subcommands."""
190
+ setup_subparsers = setup_parser.add_subparsers(dest="tool", help="AI tool to set up")
191
+ setup_subparsers.add_parser("claude-code", help="Set up Weco skill for Claude Code")
192
+
193
+
175
194
  def configure_resume_parser(resume_parser: argparse.ArgumentParser) -> None:
176
195
  """Configure arguments for the resume command."""
177
196
  resume_parser.add_argument(
@@ -202,11 +221,26 @@ Example:
202
221
  Supported provider names: {supported_providers}.
203
222
  """,
204
223
  )
224
+ resume_parser.add_argument(
225
+ "--output",
226
+ type=str,
227
+ choices=["rich", "plain"],
228
+ default="rich",
229
+ help="Output mode: 'rich' for interactive terminal UI (default), 'plain' for machine-readable text output suitable for LLM agents.",
230
+ )
205
231
 
206
232
 
207
233
  def execute_run_command(args: argparse.Namespace) -> None:
208
234
  """Execute the 'weco run' command with all its logic."""
209
- from .optimizer import execute_optimization
235
+ from .optimizer import optimize
236
+
237
+ # Early validation — fail fast with helpful errors
238
+ try:
239
+ validate_source_file(args.source)
240
+ validate_log_directory(args.log_dir)
241
+ except ValidationError as e:
242
+ print_validation_error(e, console)
243
+ sys.exit(1)
210
244
 
211
245
  try:
212
246
  api_keys = parse_api_keys(args.api_key)
@@ -225,7 +259,7 @@ def execute_run_command(args: argparse.Namespace) -> None:
225
259
  if api_keys:
226
260
  console.print(f"[bold yellow]Custom API keys provided. Using default model: {model} for the run.[/]")
227
261
 
228
- success = execute_optimization(
262
+ success = optimize(
229
263
  source=args.source,
230
264
  eval_command=args.eval_command,
231
265
  metric=args.metric,
@@ -234,12 +268,14 @@ def execute_run_command(args: argparse.Namespace) -> None:
234
268
  steps=args.steps,
235
269
  log_dir=args.log_dir,
236
270
  additional_instructions=args.additional_instructions,
237
- console=console,
238
271
  eval_timeout=args.eval_timeout,
239
272
  save_logs=args.save_logs,
240
- apply_change=args.apply_change,
241
273
  api_keys=api_keys,
274
+ apply_change=args.apply_change,
275
+ require_review=args.require_review,
276
+ output_mode=args.output,
242
277
  )
278
+
243
279
  exit_code = 0 if success else 1
244
280
  sys.exit(exit_code)
245
281
 
@@ -254,12 +290,25 @@ def execute_resume_command(args: argparse.Namespace) -> None:
254
290
  console.print(f"[bold red]Error parsing API keys: {e}[/]")
255
291
  sys.exit(1)
256
292
 
257
- success = resume_optimization(run_id=args.run_id, console=console, api_keys=api_keys, apply_change=args.apply_change)
293
+ success = resume_optimization(
294
+ run_id=args.run_id, api_keys=api_keys, apply_change=args.apply_change, output_mode=args.output
295
+ )
296
+
258
297
  sys.exit(0 if success else 1)
259
298
 
260
299
 
261
300
  def main() -> None:
262
301
  """Main function for the Weco CLI."""
302
+ try:
303
+ _main()
304
+ except KeyboardInterrupt:
305
+ # Clean exit on Ctrl+C without traceback
306
+ console.print("\n[yellow]Interrupted.[/]")
307
+ sys.exit(130) # Standard exit code for SIGINT
308
+
309
+
310
+ def _main() -> None:
311
+ """Internal main function containing the CLI logic."""
263
312
  check_for_cli_updates()
264
313
 
265
314
  parser = argparse.ArgumentParser(
@@ -277,6 +326,9 @@ def main() -> None:
277
326
  )
278
327
  configure_run_parser(run_parser) # Use the helper to add arguments
279
328
 
329
+ # --- Login Command Parser Setup ---
330
+ _ = subparsers.add_parser("login", help="Log in to Weco and save your API key.")
331
+
280
332
  # --- Logout Command Parser Setup ---
281
333
  _ = subparsers.add_parser("logout", help="Log out from Weco and clear saved API key.")
282
334
 
@@ -293,9 +345,26 @@ def main() -> None:
293
345
  )
294
346
  configure_resume_parser(resume_parser)
295
347
 
348
+ # --- Setup Command Parser Setup ---
349
+ setup_parser = subparsers.add_parser("setup", help="Set up Weco for use with AI tools")
350
+ configure_setup_parser(setup_parser)
351
+
296
352
  args = parser.parse_args()
297
353
 
298
- if args.command == "logout":
354
+ if args.command == "login":
355
+ # Check if already logged in
356
+ existing_key = load_weco_api_key()
357
+ if existing_key:
358
+ console.print("[bold green]You are already logged in.[/]")
359
+ console.print("[dim]Use 'weco logout' to log out first if you want to switch accounts.[/]")
360
+ sys.exit(0)
361
+
362
+ # Perform the login flow
363
+ if perform_login(console):
364
+ sys.exit(0)
365
+ else:
366
+ sys.exit(1)
367
+ elif args.command == "logout":
299
368
  clear_api_key()
300
369
  sys.exit(0)
301
370
  elif args.command == "run":
@@ -307,6 +376,11 @@ def main() -> None:
307
376
  sys.exit(0)
308
377
  elif args.command == "resume":
309
378
  execute_resume_command(args)
379
+ elif args.command == "setup":
380
+ from .setup import handle_setup_command
381
+
382
+ handle_setup_command(args, console)
383
+ sys.exit(0)
310
384
  else:
311
385
  # This case should be hit if 'weco' is run alone and chatbot logic didn't catch it,
312
386
  # or if an invalid command is provided.