fleet-python 0.2.79__py3-none-any.whl → 0.2.80__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.
@@ -5,6 +5,14 @@ import sys
5
5
  from typing import Dict, Tuple, Optional
6
6
 
7
7
 
8
+ # Marker for storing leading content (docstrings, imports) in the Python file
9
+ LEADING_CONTENT_START = "# @LEADING_CONTENT_START"
10
+ LEADING_CONTENT_END = "# @LEADING_CONTENT_END"
11
+ # Legacy markers for backwards compatibility
12
+ LEADING_DOCSTRING_START = "# @LEADING_DOCSTRING_START"
13
+ LEADING_DOCSTRING_END = "# @LEADING_DOCSTRING_END"
14
+
15
+
8
16
  def extract_function_info(function_code: str) -> Optional[Tuple[str, bool]]:
9
17
  """
10
18
  Extract function name and async status from Python function code.
@@ -45,18 +53,61 @@ def extract_function_info(function_code: str) -> Optional[Tuple[str, bool]]:
45
53
  return None
46
54
 
47
55
 
48
- def clean_verifier_code(code: str) -> str:
56
+ def extract_leading_content(code: str) -> Tuple[Optional[str], str]:
57
+ """
58
+ Extract leading content (docstrings, imports, etc.) before the main function definition.
59
+
60
+ Args:
61
+ code: The verifier code
62
+
63
+ Returns:
64
+ Tuple of (leading_content or None, function_code)
65
+ """
66
+ code = code.strip()
67
+
68
+ # Find the first top-level function definition (def or async def at column 0)
69
+ # We need to find "def " or "async def " that's not indented
70
+ lines = code.split("\n")
71
+ func_start_idx = None
72
+
73
+ for i, line in enumerate(lines):
74
+ # Check for unindented def or async def
75
+ if line.startswith("def ") or line.startswith("async def "):
76
+ func_start_idx = i
77
+ break
78
+
79
+ if func_start_idx is None or func_start_idx == 0:
80
+ # No leading content or function not found
81
+ return (None, code)
82
+
83
+ # Everything before the function is leading content
84
+ leading_lines = lines[:func_start_idx]
85
+ func_lines = lines[func_start_idx:]
86
+
87
+ # Clean up leading content - remove empty lines at the end
88
+ while leading_lines and not leading_lines[-1].strip():
89
+ leading_lines.pop()
90
+
91
+ if not leading_lines:
92
+ return (None, code)
93
+
94
+ leading_content = "\n".join(leading_lines)
95
+ function_code = "\n".join(func_lines)
96
+
97
+ return (leading_content, function_code)
98
+
99
+
100
+ def clean_verifier_code(code: str) -> Tuple[str, Optional[str]]:
49
101
  """
50
- Clean verifier code by removing markdown code fences and normalizing whitespace.
102
+ Clean verifier code by removing markdown code fences and extracting leading content.
51
103
 
52
104
  Args:
53
105
  code: Raw verifier code string
54
106
 
55
107
  Returns:
56
- Cleaned code string
108
+ Tuple of (function code, leading_content or None)
57
109
  """
58
- # Normalize escaped newlines
59
- code = code.replace("\\n", "\n").strip()
110
+ code = code.strip()
60
111
 
61
112
  # Remove markdown code fences if present
62
113
  if "```" in code:
@@ -64,7 +115,75 @@ def clean_verifier_code(code: str) -> str:
64
115
  if fence_blocks:
65
116
  code = fence_blocks[0].strip()
66
117
 
67
- return code
118
+ # Extract leading content (docstrings, imports, etc.) if present
119
+ leading_content, code = extract_leading_content(code)
120
+
121
+ return (code, leading_content)
122
+
123
+
124
+ def format_leading_content_as_comment(content: str) -> str:
125
+ """
126
+ Format leading content (docstrings, imports, etc.) as a comment block with markers.
127
+
128
+ Args:
129
+ content: The leading content (docstrings, imports, etc.)
130
+
131
+ Returns:
132
+ Formatted comment block
133
+ """
134
+ lines = [LEADING_CONTENT_START]
135
+
136
+ for line in content.split("\n"):
137
+ # Prefix each line with "# |" to preserve exact content including empty lines
138
+ lines.append(f"# |{line}")
139
+
140
+ lines.append(LEADING_CONTENT_END)
141
+ return "\n".join(lines)
142
+
143
+
144
+ def parse_leading_content_from_comments(comment_block: str) -> str:
145
+ """
146
+ Parse leading content from a comment block with markers.
147
+
148
+ Args:
149
+ comment_block: The comment block between markers
150
+
151
+ Returns:
152
+ Reconstructed leading content
153
+ """
154
+ lines = []
155
+ for line in comment_block.split("\n"):
156
+ # Remove "# |" prefix (new format)
157
+ if line.startswith("# |"):
158
+ lines.append(line[3:])
159
+ # Legacy format: "# " prefix
160
+ elif line.startswith("# "):
161
+ lines.append(line[2:])
162
+ elif line == "#":
163
+ lines.append("")
164
+
165
+ return "\n".join(lines)
166
+
167
+
168
+ def parse_legacy_docstring_from_comments(comment_block: str) -> str:
169
+ """
170
+ Parse a docstring from legacy comment block with markers.
171
+
172
+ Args:
173
+ comment_block: The comment block between markers
174
+
175
+ Returns:
176
+ Reconstructed docstring with triple quotes
177
+ """
178
+ lines = []
179
+ for line in comment_block.split("\n"):
180
+ # Remove "# " prefix
181
+ if line.startswith("# "):
182
+ lines.append(line[2:])
183
+ elif line == "#":
184
+ lines.append("")
185
+
186
+ return '"""' + "\n".join(lines) + '"""'
68
187
 
69
188
 
70
189
  def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
@@ -123,8 +242,8 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
123
242
  missing_verifier.append(task_key)
124
243
  continue
125
244
 
126
- # Clean the code
127
- cleaned_code = clean_verifier_code(verifier_code)
245
+ # Clean the code and extract leading content (docstrings, imports, etc.)
246
+ cleaned_code, leading_content = clean_verifier_code(verifier_code)
128
247
 
129
248
  # Extract function info
130
249
  func_info = extract_function_info(cleaned_code)
@@ -142,6 +261,7 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
142
261
  "function_name": function_name,
143
262
  "is_async": is_async,
144
263
  "code": cleaned_code,
264
+ "leading_content": leading_content,
145
265
  }
146
266
  )
147
267
 
@@ -195,7 +315,7 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
195
315
  f.write("import json\n")
196
316
  f.write("import re\n")
197
317
  f.write("import string\n")
198
- f.write("from typing import Any\n")
318
+ f.write("from typing import Any, Dict, List\n")
199
319
  f.write("\n")
200
320
  f.write("# Helper functions available in verifier namespace\n")
201
321
  f.write(
@@ -248,6 +368,11 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
248
368
  f"# Function: {ver['function_name']} ({'async' if ver['is_async'] else 'sync'})\n"
249
369
  )
250
370
 
371
+ # Write leading content (docstrings, imports) as comments if present
372
+ if ver["leading_content"]:
373
+ f.write(format_leading_content_as_comment(ver["leading_content"]))
374
+ f.write("\n")
375
+
251
376
  # Write decorator - use verifier for async, verifier_sync for sync
252
377
  decorator_name = "verifier" if ver["is_async"] else "verifier_sync"
253
378
  f.write(f'@{decorator_name}(key="{ver["task_key"]}")\n')
@@ -262,7 +387,7 @@ def extract_verifiers_to_file(json_path: str, py_path: str) -> None:
262
387
  print(f" 2. Run: python {sys.argv[0]} apply {json_path} {py_path}")
263
388
 
264
389
 
265
- def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
390
+ def parse_verifiers_from_file(python_path: str) -> Dict[str, dict]:
266
391
  """
267
392
  Parse verifiers from a Python file and extract them by task key.
268
393
 
@@ -270,7 +395,7 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
270
395
  python_path: Path to Python file containing verifiers
271
396
 
272
397
  Returns:
273
- Dictionary mapping task_key to verifier code
398
+ Dictionary mapping task_key to dict with 'code' and 'leading_content'
274
399
  """
275
400
  print(f"Reading verifiers from: {python_path}")
276
401
 
@@ -281,14 +406,15 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
281
406
  print(f"✗ Error: File '{python_path}' not found")
282
407
  sys.exit(1)
283
408
 
284
- # Split content by the separator comments to get individual verifier sections
285
- # The separator is "# ------------------------------------------------------------------------------"
286
- # Each section starts with "# Task: <key>"
287
-
288
409
  verifiers = {}
289
410
 
290
- # Split by "# Task: " markers to find each verifier block
291
- task_blocks = re.split(r"\n# Task: ", content)
411
+ # Split by "# Task: " markers followed by a task key pattern (uuid or specific format)
412
+ # This avoids splitting on "# Task: " that appears inside docstring comments
413
+ # Task keys look like: task_uuid, task_xxx_timestamp_xxx, or send_xxx_xxx
414
+ task_key_pattern = (
415
+ r"(?:task_[a-f0-9-]+|task_[a-z0-9]+_\d+_[a-z0-9]+|[a-z_]+_[a-z0-9]+)"
416
+ )
417
+ task_blocks = re.split(rf"\n# Task: (?={task_key_pattern})", content)
292
418
 
293
419
  for block in task_blocks[1:]: # Skip the first block (header)
294
420
  # Extract task key from the first line
@@ -299,6 +425,10 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
299
425
  # First line should be the task key
300
426
  task_key = lines[0].strip()
301
427
 
428
+ # Skip if this doesn't look like a task key (sanity check)
429
+ if not re.match(task_key_pattern, task_key):
430
+ continue
431
+
302
432
  # Find the @verifier or @verifier_sync decorator to extract the key parameter
303
433
  verifier_match = re.search(
304
434
  r'@verifier(?:_sync)?\(key=["\']([^"\']+)["\']\s*(?:,\s*[^)]+)?\)', block
@@ -306,6 +436,26 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
306
436
  if verifier_match:
307
437
  task_key = verifier_match.group(1)
308
438
 
439
+ # Check for leading content markers (new format)
440
+ leading_content = None
441
+ if LEADING_CONTENT_START in block:
442
+ start_idx = block.find(LEADING_CONTENT_START)
443
+ end_idx = block.find(LEADING_CONTENT_END)
444
+ if start_idx != -1 and end_idx != -1:
445
+ comment_block = block[
446
+ start_idx + len(LEADING_CONTENT_START) : end_idx
447
+ ].strip()
448
+ leading_content = parse_leading_content_from_comments(comment_block)
449
+ # Fallback: check for legacy docstring markers
450
+ elif LEADING_DOCSTRING_START in block:
451
+ start_idx = block.find(LEADING_DOCSTRING_START)
452
+ end_idx = block.find(LEADING_DOCSTRING_END)
453
+ if start_idx != -1 and end_idx != -1:
454
+ comment_block = block[
455
+ start_idx + len(LEADING_DOCSTRING_START) : end_idx
456
+ ].strip()
457
+ leading_content = parse_legacy_docstring_from_comments(comment_block)
458
+
309
459
  # Find the function definition (async def or def)
310
460
  # Extract from the function start until we hit the separator or end
311
461
  func_pattern = r"((async\s+)?def\s+\w+.*?)(?=\n# -+\n|\n# Task:|\Z)"
@@ -313,26 +463,31 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
313
463
 
314
464
  if func_match:
315
465
  function_code = func_match.group(1).strip()
316
- verifiers[task_key] = function_code
466
+ verifiers[task_key] = {
467
+ "code": function_code,
468
+ "leading_content": leading_content,
469
+ }
317
470
 
318
471
  # If the above approach didn't work, try a direct pattern match
319
472
  if not verifiers:
320
473
  # Pattern to match @verifier or @verifier_sync decorator with key and the following function
321
- # Look for the decorator, then capture everything until we hit a dedented line or separator
322
474
  pattern = r'@verifier(?:_sync)?\(key=["\']([^"\']+)["\']\s*(?:,\s*[^)]+)?\)\s*\n((?:async\s+)?def\s+[^\n]+:(?:\n(?: |\t).*)*(?:\n(?: |\t).*)*)'
323
475
 
324
476
  matches = re.findall(pattern, content, re.MULTILINE)
325
477
 
326
478
  for task_key, function_code in matches:
327
- verifiers[task_key] = function_code.strip()
479
+ verifiers[task_key] = {
480
+ "code": function_code.strip(),
481
+ "leading_content": None,
482
+ }
328
483
 
329
484
  print(f"✓ Found {len(verifiers)} verifier(s)")
330
485
 
331
486
  # Analyze async vs sync
332
487
  async_count = 0
333
488
  sync_count = 0
334
- for code in verifiers.values():
335
- func_info = extract_function_info(code)
489
+ for data in verifiers.values():
490
+ func_info = extract_function_info(data["code"])
336
491
  if func_info:
337
492
  _, is_async = func_info
338
493
  if is_async:
@@ -346,6 +501,22 @@ def parse_verifiers_from_file(python_path: str) -> Dict[str, str]:
346
501
  return verifiers
347
502
 
348
503
 
504
+ def normalize_code_for_comparison(code: str) -> str:
505
+ """
506
+ Normalize code for comparison to avoid false positives.
507
+ Removes leading/trailing whitespace and normalizes line endings.
508
+ """
509
+ # Strip and normalize line endings
510
+ code = code.strip().replace("\r\n", "\n")
511
+ # Normalize trailing whitespace on each line
512
+ lines = code.split("\n")
513
+ lines = [line.rstrip() for line in lines]
514
+ code = "\n".join(lines)
515
+ # Normalize multiple blank lines to single (2+ newlines → 1)
516
+ code = re.sub(r"\n\n+", "\n", code)
517
+ return code
518
+
519
+
349
520
  def apply_verifiers_to_json(json_path: str, python_path: str) -> None:
350
521
  """
351
522
  Apply verifiers from Python file back into JSON task file (updates in-place).
@@ -386,17 +557,43 @@ def apply_verifiers_to_json(json_path: str, python_path: str) -> None:
386
557
  continue
387
558
 
388
559
  if task_key in verifiers:
389
- new_code = verifiers[task_key]
390
-
391
- # Escape newlines in debug print patterns (>>> and <<<)
392
- # These should be \n escape sequences, not actual newlines
393
- new_code = new_code.replace(">>>\n", ">>>\\n")
394
- new_code = new_code.replace("\n<<<", "\\n<<<")
395
-
396
- old_code = task.get("verifier_func", "").strip()
560
+ ver_data = verifiers[task_key]
561
+
562
+ # Reconstruct the full verifier code with leading content if present
563
+ if ver_data["leading_content"]:
564
+ new_code = ver_data["leading_content"] + "\n" + ver_data["code"]
565
+ else:
566
+ new_code = ver_data["code"]
567
+
568
+ old_code = task.get("verifier_func", "")
569
+
570
+ # Normalize both for comparison
571
+ old_normalized = normalize_code_for_comparison(old_code)
572
+ new_normalized = normalize_code_for_comparison(new_code)
573
+
574
+ # Debug: show comparison info
575
+ old_len = len(old_normalized)
576
+ new_len = len(new_normalized)
577
+
578
+ if old_normalized == new_normalized:
579
+ if old_code != new_code:
580
+ print(
581
+ f" [DEBUG] {task_key}: Codes differ in whitespace only (normalized match)"
582
+ )
583
+ else:
584
+ # Find first difference position for debugging
585
+ min_len = min(old_len, new_len)
586
+ diff_pos = min_len
587
+ for i in range(min_len):
588
+ if old_normalized[i] != new_normalized[i]:
589
+ diff_pos = i
590
+ break
591
+ print(
592
+ f" [DEBUG] {task_key}: Code changed (old={old_len}, new={new_len}, first_diff@{diff_pos})"
593
+ )
397
594
 
398
595
  # Only update if the code actually changed
399
- if old_code != new_code.strip():
596
+ if old_normalized != new_normalized:
400
597
  # Update verifier_func with new code
401
598
  task["verifier_func"] = new_code
402
599
 
@@ -451,14 +648,17 @@ def validate_verifiers_file(python_path: str) -> None:
451
648
  print("\nValidating verifiers...")
452
649
  errors = []
453
650
 
454
- for task_key, code in verifiers.items():
455
- func_info = extract_function_info(code)
651
+ for task_key, ver_data in verifiers.items():
652
+ func_info = extract_function_info(ver_data["code"])
456
653
  if not func_info:
457
654
  errors.append(f" - {task_key}: Could not extract function info")
458
655
  else:
459
656
  function_name, is_async = func_info
657
+ has_leading = (
658
+ " (has leading content)" if ver_data["leading_content"] else ""
659
+ )
460
660
  print(
461
- f" ✓ {task_key}: {function_name} ({'async' if is_async else 'sync'})"
661
+ f" ✓ {task_key}: {function_name} ({'async' if is_async else 'sync'}){has_leading}"
462
662
  )
463
663
 
464
664
  if errors:
fleet/__init__.py CHANGED
@@ -73,7 +73,7 @@ from . import env
73
73
  from . import global_client as _global_client
74
74
  from ._async import global_client as _async_global_client
75
75
 
76
- __version__ = "0.2.79"
76
+ __version__ = "0.2.80"
77
77
 
78
78
  __all__ = [
79
79
  # Core classes
fleet/cli.py ADDED
@@ -0,0 +1,354 @@
1
+ """Fleet CLI - Command line interface for Fleet SDK."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from typing import List, Optional
7
+
8
+ try:
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ except ImportError:
13
+ print(
14
+ "Error: CLI dependencies not installed.\n"
15
+ "Install with: pip install 'fleet-python[cli]'",
16
+ file=sys.stderr,
17
+ )
18
+ sys.exit(1)
19
+
20
+ from .client import Fleet
21
+ from .models import JobCreateRequest
22
+
23
+ app = typer.Typer(
24
+ name="flt",
25
+ help="Fleet CLI - Interact with Fleet jobs and sessions",
26
+ no_args_is_help=True,
27
+ )
28
+ jobs_app = typer.Typer(help="Manage jobs", no_args_is_help=True)
29
+ sessions_app = typer.Typer(help="Manage sessions", no_args_is_help=True)
30
+
31
+ app.add_typer(jobs_app, name="jobs")
32
+ app.add_typer(sessions_app, name="sessions")
33
+
34
+ console = Console()
35
+
36
+
37
+ CLI_DEFAULT_BASE_URL = "https://us-west-1.fleetai.com"
38
+
39
+
40
+ def get_client() -> Fleet:
41
+ """Get a Fleet client using environment variables."""
42
+ api_key = os.getenv("FLEET_API_KEY")
43
+ if not api_key:
44
+ console.print(
45
+ "[red]Error:[/red] FLEET_API_KEY environment variable not set",
46
+ style="bold",
47
+ )
48
+ raise typer.Exit(1)
49
+ base_url = os.getenv("FLEET_BASE_URL", CLI_DEFAULT_BASE_URL)
50
+ return Fleet(api_key=api_key, base_url=base_url)
51
+
52
+
53
+ # Jobs commands
54
+
55
+
56
+ @jobs_app.command("list")
57
+ def list_jobs(
58
+ team_id: Optional[str] = typer.Option(None, "--team-id", help="Filter by team ID (admin only)"),
59
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
60
+ ):
61
+ """List all jobs."""
62
+ client = get_client()
63
+ jobs = client.list_jobs(team_id=team_id)
64
+
65
+ if output_json:
66
+ console.print(json.dumps([j.model_dump() for j in jobs], indent=2, default=str))
67
+ return
68
+
69
+ if not jobs:
70
+ console.print("No jobs found.")
71
+ return
72
+
73
+ table = Table(title="Jobs")
74
+ table.add_column("ID", style="cyan")
75
+ table.add_column("Name", style="green")
76
+ table.add_column("Status", style="yellow")
77
+ table.add_column("Created At", style="dim")
78
+
79
+ for job in jobs:
80
+ table.add_row(
81
+ job.id,
82
+ job.name or "-",
83
+ job.status or "-",
84
+ job.created_at or "-",
85
+ )
86
+
87
+ console.print(table)
88
+
89
+
90
+ @jobs_app.command("create")
91
+ def create_job(
92
+ model: List[str] = typer.Option(..., "--model", "-m", help="Model in 'provider/model' format (repeatable)"),
93
+ env_key: Optional[str] = typer.Option(None, "--env-key", "-e", help="Environment key"),
94
+ project_key: Optional[str] = typer.Option(None, "--project-key", "-p", help="Project key"),
95
+ task_keys: Optional[List[str]] = typer.Option(None, "--task-key", "-t", help="Task key (repeatable)"),
96
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Job name. Supports placeholders: {id} (UUID), {sid} (short UUID), {i} (auto-increment, must be suffix)"),
97
+ pass_k: int = typer.Option(1, "--pass-k", help="Number of passes"),
98
+ max_steps: Optional[int] = typer.Option(None, "--max-steps", help="Maximum agent steps"),
99
+ max_duration: int = typer.Option(60, "--max-duration", help="Timeout in minutes"),
100
+ max_concurrent: int = typer.Option(30, "--max-concurrent", help="Max concurrent per model"),
101
+ mode: Optional[str] = typer.Option(None, "--mode", help="Mode: 'tool-use' or 'computer-use'"),
102
+ system_prompt: Optional[str] = typer.Option(None, "--system-prompt", help="Custom system prompt"),
103
+ model_prompt: Optional[List[str]] = typer.Option(None, "--model-prompt", help="Per-model prompt in 'provider/model=prompt' format (repeatable)"),
104
+ byok: Optional[List[str]] = typer.Option(None, "--byok", help="Bring Your Own Key in 'provider=key' format (repeatable)"),
105
+ byok_ttl: Optional[int] = typer.Option(None, "--byok-ttl", help="TTL for BYOK keys in minutes"),
106
+ harness: Optional[str] = typer.Option(None, "--harness", help="Harness identifier"),
107
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
108
+ ):
109
+ """Create a new job.
110
+
111
+ Requires --model (repeatable) and exactly one of --env-key, --project-key, or --task-key.
112
+ """
113
+ # Validate mutual exclusivity
114
+ sources = [env_key, project_key, task_keys]
115
+ specified = sum(1 for s in sources if s)
116
+ if specified != 1:
117
+ console.print(
118
+ "[red]Error:[/red] Exactly one of --env-key, --project-key, or --task-key must be specified",
119
+ style="bold",
120
+ )
121
+ raise typer.Exit(1)
122
+
123
+ # Parse model prompts
124
+ model_prompts = None
125
+ if model_prompt:
126
+ model_prompts = {}
127
+ for mp in model_prompt:
128
+ if "=" not in mp:
129
+ console.print(
130
+ f"[red]Error:[/red] Invalid --model-prompt format: {mp}. Expected 'provider/model=prompt'",
131
+ style="bold",
132
+ )
133
+ raise typer.Exit(1)
134
+ key, value = mp.split("=", 1)
135
+ model_prompts[key] = value
136
+
137
+ # Parse BYOK keys
138
+ byok_keys = None
139
+ if byok:
140
+ byok_keys = {}
141
+ for b in byok:
142
+ if "=" not in b:
143
+ console.print(
144
+ f"[red]Error:[/red] Invalid --byok format: {b}. Expected 'provider=key'",
145
+ style="bold",
146
+ )
147
+ raise typer.Exit(1)
148
+ provider, key = b.split("=", 1)
149
+ byok_keys[provider] = key
150
+
151
+ client = get_client()
152
+ result = client.create_job(
153
+ models=model,
154
+ name=name,
155
+ pass_k=pass_k,
156
+ env_key=env_key,
157
+ project_key=project_key,
158
+ task_keys=task_keys,
159
+ max_steps=max_steps,
160
+ max_duration_minutes=max_duration,
161
+ max_concurrent_per_model=max_concurrent,
162
+ mode=mode,
163
+ system_prompt=system_prompt,
164
+ model_prompts=model_prompts,
165
+ byok_keys=byok_keys,
166
+ byok_ttl_minutes=byok_ttl,
167
+ harness=harness,
168
+ )
169
+
170
+ if output_json:
171
+ console.print(json.dumps(result.model_dump(), indent=2, default=str))
172
+ return
173
+
174
+ console.print(f"[green]Job created successfully![/green]")
175
+ console.print(f" Job ID: [cyan]{result.job_id}[/cyan]")
176
+ console.print(f" Workflow ID: {result.workflow_job_id}")
177
+ console.print(f" Status: [yellow]{result.status}[/yellow]")
178
+ if result.name:
179
+ console.print(f" Name: {result.name}")
180
+
181
+
182
+ @jobs_app.command("get")
183
+ def get_job(
184
+ job_id: str = typer.Argument(..., help="Job ID"),
185
+ team_id: Optional[str] = typer.Option(None, "--team-id", help="Team ID (admin only)"),
186
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
187
+ ):
188
+ """Get details for a specific job."""
189
+ client = get_client()
190
+ job = client.get_job(job_id, team_id=team_id)
191
+
192
+ if output_json:
193
+ console.print(json.dumps(job.model_dump(), indent=2, default=str))
194
+ return
195
+
196
+ console.print(f"[bold]Job Details[/bold]")
197
+ console.print(f" ID: [cyan]{job.id}[/cyan]")
198
+ console.print(f" Name: {job.name or '-'}")
199
+ console.print(f" Status: [yellow]{job.status or '-'}[/yellow]")
200
+ console.print(f" Created At: {job.created_at or '-'}")
201
+
202
+
203
+ @jobs_app.command("sessions")
204
+ def list_job_sessions(
205
+ job_id: str = typer.Argument(..., help="Job ID"),
206
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
207
+ ):
208
+ """List all sessions for a job, grouped by task."""
209
+ client = get_client()
210
+ result = client.list_job_sessions(job_id)
211
+
212
+ if output_json:
213
+ console.print(json.dumps(result.model_dump(), indent=2, default=str))
214
+ return
215
+
216
+ console.print(f"[bold]Sessions for Job:[/bold] [cyan]{result.job_id}[/cyan]")
217
+ console.print(f"Total Sessions: {result.total_sessions}\n")
218
+
219
+ for task_group in result.tasks:
220
+ task_name = task_group.task.key if task_group.task else task_group.task_id or "Unknown"
221
+ pass_rate_pct = task_group.pass_rate * 100
222
+
223
+ console.print(f"[bold green]Task:[/bold green] {task_name}")
224
+ console.print(f" Pass Rate: {task_group.passed_sessions}/{task_group.total_sessions} ({pass_rate_pct:.1f}%)")
225
+ if task_group.average_score is not None:
226
+ console.print(f" Average Score: {task_group.average_score:.2f}")
227
+
228
+ table = Table(show_header=True)
229
+ table.add_column("Session ID", style="cyan")
230
+ table.add_column("Model", style="blue")
231
+ table.add_column("Status", style="yellow")
232
+ table.add_column("Steps")
233
+ table.add_column("Result")
234
+
235
+ for session in task_group.sessions:
236
+ result_str = "-"
237
+ if session.verifier_execution:
238
+ if session.verifier_execution.success:
239
+ result_str = "[green]PASS[/green]"
240
+ if session.verifier_execution.score is not None:
241
+ result_str += f" ({session.verifier_execution.score:.2f})"
242
+ else:
243
+ result_str = "[red]FAIL[/red]"
244
+
245
+ table.add_row(
246
+ session.session_id[:8] + "...",
247
+ session.model,
248
+ session.status,
249
+ str(session.step_count),
250
+ result_str,
251
+ )
252
+
253
+ console.print(table)
254
+ console.print()
255
+
256
+
257
+ # Sessions commands
258
+
259
+
260
+ @sessions_app.command("transcript")
261
+ def get_session_transcript(
262
+ session_id: str = typer.Argument(..., help="Session ID"),
263
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
264
+ ):
265
+ """Get the transcript for a session."""
266
+ client = get_client()
267
+ result = client.get_session_transcript(session_id)
268
+
269
+ if output_json:
270
+ console.print(json.dumps(result.model_dump(), indent=2, default=str))
271
+ return
272
+
273
+ # Header
274
+ console.print(f"[bold]Session Transcript[/bold]")
275
+ console.print()
276
+
277
+ # Task info
278
+ if result.task:
279
+ console.print(f"[bold]Task:[/bold] {result.task.key}")
280
+ console.print(f" Environment: {result.task.env_id}")
281
+ if result.task.version:
282
+ console.print(f" Version: {result.task.version}")
283
+ console.print()
284
+ console.print(f"[bold]Prompt:[/bold]")
285
+ console.print(f" {result.task.prompt[:200]}{'...' if len(result.task.prompt) > 200 else ''}")
286
+ console.print()
287
+
288
+ # Verifier result
289
+ if result.verifier_execution:
290
+ status = "[green]PASS[/green]" if result.verifier_execution.success else "[red]FAIL[/red]"
291
+ console.print(f"[bold]Verifier Result:[/bold] {status}")
292
+ if result.verifier_execution.score is not None:
293
+ console.print(f" Score: {result.verifier_execution.score}")
294
+ console.print(f" Execution Time: {result.verifier_execution.execution_time_ms}ms")
295
+ console.print()
296
+
297
+ # Transcript
298
+ console.print(f"[bold]Transcript:[/bold] ({len(result.transcript)} messages)")
299
+ console.print("-" * 60)
300
+
301
+ for msg in result.transcript:
302
+ role_colors = {
303
+ "user": "green",
304
+ "assistant": "blue",
305
+ "tool": "yellow",
306
+ "system": "magenta",
307
+ }
308
+ color = role_colors.get(msg.role, "white")
309
+ console.print(f"[bold {color}]{msg.role.upper()}:[/bold {color}]")
310
+
311
+ # Handle content
312
+ if isinstance(msg.content, str):
313
+ # Truncate long content
314
+ content = msg.content
315
+ if len(content) > 500:
316
+ content = content[:500] + "..."
317
+ console.print(f" {content}")
318
+ elif isinstance(msg.content, list):
319
+ # Multimodal content
320
+ for item in msg.content:
321
+ if isinstance(item, dict):
322
+ if item.get("type") == "text":
323
+ text = item.get("text", "")
324
+ if len(text) > 500:
325
+ text = text[:500] + "..."
326
+ console.print(f" {text}")
327
+ elif item.get("type") == "image_url":
328
+ console.print(f" [dim][Image][/dim]")
329
+ elif item.get("type") == "tool_use":
330
+ console.print(f" [dim]Tool: {item.get('name', 'unknown')}[/dim]")
331
+ elif item.get("type") == "tool_result":
332
+ console.print(f" [dim]Tool Result[/dim]")
333
+ else:
334
+ console.print(f" {item}")
335
+ else:
336
+ console.print(f" {msg.content}")
337
+
338
+ # Tool calls
339
+ if msg.tool_calls:
340
+ for tc in msg.tool_calls:
341
+ if isinstance(tc, dict):
342
+ name = tc.get("function", {}).get("name", tc.get("name", "unknown"))
343
+ console.print(f" [dim]-> Tool call: {name}[/dim]")
344
+
345
+ console.print()
346
+
347
+
348
+ def main():
349
+ """Entry point for the CLI."""
350
+ app()
351
+
352
+
353
+ if __name__ == "__main__":
354
+ main()
fleet/client.py CHANGED
@@ -38,6 +38,12 @@ from .models import (
38
38
  TaskUpdateRequest,
39
39
  Run,
40
40
  HeartbeatResponse,
41
+ JobCreateRequest,
42
+ JobResponse,
43
+ JobListResponse,
44
+ JobCreateResponse,
45
+ JobSessionsResponse,
46
+ SessionTranscriptResponse,
41
47
  )
42
48
  from .tasks import Task
43
49
 
@@ -1033,6 +1039,136 @@ class Fleet:
1033
1039
  )
1034
1040
  return TaskResponse(**response.json())
1035
1041
 
1042
+ # Jobs API methods
1043
+
1044
+ def list_jobs(self, team_id: Optional[str] = None) -> List[JobResponse]:
1045
+ """List all jobs for the authenticated team.
1046
+
1047
+ Args:
1048
+ team_id: Optional team_id to filter by (admin only)
1049
+
1050
+ Returns:
1051
+ List[JobResponse] containing job information
1052
+ """
1053
+ params = {}
1054
+ if team_id is not None:
1055
+ params["team_id"] = team_id
1056
+
1057
+ response = self.client.request("GET", "/v1/jobs", params=params)
1058
+ job_list = JobListResponse(**response.json())
1059
+ return job_list.jobs
1060
+
1061
+ def create_job(
1062
+ self,
1063
+ models: List[str],
1064
+ name: Optional[str] = None,
1065
+ pass_k: int = 1,
1066
+ env_key: Optional[str] = None,
1067
+ project_key: Optional[str] = None,
1068
+ task_keys: Optional[List[str]] = None,
1069
+ excluded_task_keys: Optional[List[str]] = None,
1070
+ max_steps: Optional[int] = None,
1071
+ max_duration_minutes: int = 60,
1072
+ max_concurrent_per_model: int = 30,
1073
+ mode: Optional[str] = None,
1074
+ system_prompt: Optional[str] = None,
1075
+ model_prompts: Optional[Dict[str, str]] = None,
1076
+ byok_keys: Optional[Dict[str, str]] = None,
1077
+ byok_ttl_minutes: Optional[int] = None,
1078
+ harness: Optional[str] = None,
1079
+ ) -> JobCreateResponse:
1080
+ """Create a new job.
1081
+
1082
+ Args:
1083
+ models: List of model identifiers in "provider/model" format
1084
+ name: Optional job name. Supports placeholders: {id} (UUID), {sid} (short UUID), {i} (auto-increment, must be suffix)
1085
+ pass_k: Number of passes (default: 1)
1086
+ env_key: Environment key (mutually exclusive with project_key/task_keys)
1087
+ project_key: Project key (mutually exclusive with env_key/task_keys)
1088
+ task_keys: Specific task keys (mutually exclusive with env_key/project_key)
1089
+ excluded_task_keys: Task keys to exclude
1090
+ max_steps: Maximum agent steps
1091
+ max_duration_minutes: Timeout in minutes (default: 60)
1092
+ max_concurrent_per_model: Max concurrent per model (default: 30)
1093
+ mode: "tool-use" or "computer-use"
1094
+ system_prompt: Custom system prompt
1095
+ model_prompts: Per-model prompts (model -> prompt)
1096
+ byok_keys: Bring Your Own Keys (provider -> API key)
1097
+ byok_ttl_minutes: TTL for BYOK keys in minutes
1098
+ harness: Harness identifier
1099
+
1100
+ Returns:
1101
+ JobCreateResponse containing job_id, workflow_job_id, status, and name
1102
+ """
1103
+ request = JobCreateRequest(
1104
+ name=name,
1105
+ models=models,
1106
+ pass_k=pass_k,
1107
+ env_key=env_key,
1108
+ project_key=project_key,
1109
+ task_keys=task_keys,
1110
+ excluded_task_keys=excluded_task_keys,
1111
+ max_steps=max_steps,
1112
+ max_duration_minutes=max_duration_minutes,
1113
+ max_concurrent_per_model=max_concurrent_per_model,
1114
+ mode=mode,
1115
+ system_prompt=system_prompt,
1116
+ model_prompts=model_prompts,
1117
+ byok_keys=byok_keys,
1118
+ byok_ttl_minutes=byok_ttl_minutes,
1119
+ harness=harness,
1120
+ )
1121
+
1122
+ response = self.client.request(
1123
+ "POST", "/v1/jobs", json=request.model_dump(exclude_none=True)
1124
+ )
1125
+ return JobCreateResponse(**response.json())
1126
+
1127
+ def get_job(self, job_id: str, team_id: Optional[str] = None) -> JobResponse:
1128
+ """Get a specific job by ID.
1129
+
1130
+ Args:
1131
+ job_id: The job ID
1132
+ team_id: Optional team_id to filter by (admin only)
1133
+
1134
+ Returns:
1135
+ JobResponse containing job information
1136
+ """
1137
+ params = {}
1138
+ if team_id is not None:
1139
+ params["team_id"] = team_id
1140
+
1141
+ response = self.client.request("GET", f"/v1/jobs/{job_id}", params=params)
1142
+ return JobResponse(**response.json())
1143
+
1144
+ # Sessions API methods
1145
+
1146
+ def list_job_sessions(self, job_id: str) -> JobSessionsResponse:
1147
+ """List all sessions for a job, grouped by task.
1148
+
1149
+ Args:
1150
+ job_id: The job ID
1151
+
1152
+ Returns:
1153
+ JobSessionsResponse containing sessions grouped by task with statistics
1154
+ """
1155
+ response = self.client.request("GET", f"/v1/sessions/job/{job_id}")
1156
+ return JobSessionsResponse(**response.json())
1157
+
1158
+ def get_session_transcript(self, session_id: str) -> SessionTranscriptResponse:
1159
+ """Get the transcript for a specific session.
1160
+
1161
+ Args:
1162
+ session_id: The session ID
1163
+
1164
+ Returns:
1165
+ SessionTranscriptResponse containing task, instance, verifier result, and messages
1166
+ """
1167
+ response = self.client.request(
1168
+ "GET", f"/v1/sessions/{session_id}/transcript"
1169
+ )
1170
+ return SessionTranscriptResponse(**response.json())
1171
+
1036
1172
  def _create_verifier_from_data(
1037
1173
  self, verifier_id: str, verifier_key: str, verifier_code: str, verifier_sha: str, verifier_runtime_version: Optional[str] = None
1038
1174
  ) -> "SyncVerifierFunction":
fleet/models.py CHANGED
@@ -405,3 +405,138 @@ class AccountResponse(BaseModel):
405
405
  instance_count: int = Field(..., title="Instance Count")
406
406
  profile_id: Optional[str] = Field(None, title="Profile Id")
407
407
  profile_name: Optional[str] = Field(None, title="Profile Name")
408
+
409
+
410
+ # Jobs and Sessions models
411
+
412
+
413
+ class JobCreateRequest(BaseModel):
414
+ """Request payload for creating a new job.
415
+
416
+ The name field supports placeholders:
417
+ - {id}: Replaced with a full UUID
418
+ - {sid}: Replaced with the first 8 characters of a UUID (short ID)
419
+ - {i}: Replaced with auto-incrementing number (must be a suffix, e.g., "job-{i}")
420
+ """
421
+
422
+ name: Optional[str] = Field(None, title="Name", max_length=255)
423
+ models: List[str] = Field(..., title="Models", min_length=1)
424
+ pass_k: int = Field(1, title="Pass K", ge=1)
425
+ env_key: Optional[str] = Field(None, title="Env Key")
426
+ project_key: Optional[str] = Field(None, title="Project Key")
427
+ task_keys: Optional[List[str]] = Field(None, title="Task Keys", min_length=1)
428
+ excluded_task_keys: Optional[List[str]] = Field(None, title="Excluded Task Keys")
429
+ max_steps: Optional[int] = Field(None, title="Max Steps", ge=1)
430
+ max_duration_minutes: int = Field(60, title="Max Duration Minutes", ge=1)
431
+ max_concurrent_per_model: int = Field(30, title="Max Concurrent Per Model", ge=1)
432
+ mode: Optional[str] = Field(None, title="Mode")
433
+ system_prompt: Optional[str] = Field(None, title="System Prompt")
434
+ model_prompts: Optional[Dict[str, str]] = Field(None, title="Model Prompts")
435
+ byok_keys: Optional[Dict[str, str]] = Field(None, title="BYOK Keys")
436
+ byok_ttl_minutes: Optional[int] = Field(None, title="BYOK TTL Minutes", ge=1)
437
+ harness: Optional[str] = Field(None, title="Harness")
438
+
439
+
440
+ class JobResponse(BaseModel):
441
+ """Response for a single job."""
442
+
443
+ id: str = Field(..., title="Id")
444
+ name: Optional[str] = Field(None, title="Name")
445
+ created_at: Optional[str] = Field(None, title="Created At")
446
+ status: Optional[str] = Field(None, title="Status")
447
+
448
+
449
+ class JobListResponse(BaseModel):
450
+ """Response for listing jobs."""
451
+
452
+ jobs: List[JobResponse] = Field(..., title="Jobs")
453
+ total: int = Field(..., title="Total")
454
+
455
+
456
+ class JobCreateResponse(BaseModel):
457
+ """Response from creating a job."""
458
+
459
+ job_id: str = Field(..., title="Job Id")
460
+ workflow_job_id: str = Field(..., title="Workflow Job Id")
461
+ status: str = Field(..., title="Status")
462
+ name: Optional[str] = Field(None, title="Name")
463
+
464
+
465
+ class VerifierExecutionResult(BaseModel):
466
+ """Verifier execution result for a session."""
467
+
468
+ success: bool = Field(..., title="Success")
469
+ score: Optional[float] = Field(None, title="Score")
470
+ stdout: Optional[str] = Field(None, title="Stdout")
471
+ execution_time_ms: int = Field(..., title="Execution Time Ms")
472
+ result: Optional[Any] = Field(None, title="Result")
473
+
474
+
475
+ class SessionInfo(BaseModel):
476
+ """Session information within a job."""
477
+
478
+ session_id: str = Field(..., title="Session Id")
479
+ instance: Optional[Instance] = Field(None, title="Instance")
480
+ model: str = Field(..., title="Model")
481
+ status: str = Field(..., title="Status")
482
+ created_at: str = Field(..., title="Created At")
483
+ started_at: Optional[str] = Field(None, title="Started At")
484
+ ended_at: Optional[str] = Field(None, title="Ended At")
485
+ step_count: int = Field(..., title="Step Count")
486
+ verifier_execution: Optional[VerifierExecutionResult] = Field(
487
+ None, title="Verifier Execution"
488
+ )
489
+
490
+
491
+ class TaskInfo(BaseModel):
492
+ """Task information for session transcript."""
493
+
494
+ key: str = Field(..., title="Key")
495
+ prompt: str = Field(..., title="Prompt")
496
+ env_id: str = Field(..., title="Env Id")
497
+ env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
498
+ created_at: Optional[str] = Field(None, title="Created At")
499
+ version: Optional[str] = Field(None, title="Version")
500
+ verifier_func: Optional[str] = Field(None, title="Verifier Func")
501
+ verifier_id: Optional[str] = Field(None, title="Verifier Id")
502
+ metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
503
+
504
+
505
+ class TaskSessionGroup(BaseModel):
506
+ """Sessions grouped by task."""
507
+
508
+ task_id: Optional[str] = Field(None, title="Task Id")
509
+ task: Optional[TaskInfo] = Field(None, title="Task")
510
+ total_sessions: int = Field(..., title="Total Sessions")
511
+ passed_sessions: int = Field(..., title="Passed Sessions")
512
+ pass_rate: float = Field(..., title="Pass Rate")
513
+ average_score: Optional[float] = Field(None, title="Average Score")
514
+ sessions: List[SessionInfo] = Field(..., title="Sessions")
515
+
516
+
517
+ class JobSessionsResponse(BaseModel):
518
+ """Response for listing sessions for a job."""
519
+
520
+ job_id: str = Field(..., title="Job Id")
521
+ total_sessions: int = Field(..., title="Total Sessions")
522
+ tasks: List[TaskSessionGroup] = Field(..., title="Tasks")
523
+
524
+
525
+ class TranscriptMessage(BaseModel):
526
+ """A message in the session transcript."""
527
+
528
+ role: str = Field(..., title="Role")
529
+ content: Any = Field(..., title="Content")
530
+ tool_calls: Optional[List[Any]] = Field(None, title="Tool Calls")
531
+ tool_call_id: Optional[str] = Field(None, title="Tool Call Id")
532
+
533
+
534
+ class SessionTranscriptResponse(BaseModel):
535
+ """Response for a session transcript."""
536
+
537
+ task: Optional[TaskInfo] = Field(None, title="Task")
538
+ instance: Optional[Instance] = Field(None, title="Instance")
539
+ verifier_execution: Optional[VerifierExecutionResult] = Field(
540
+ None, title="Verifier Execution"
541
+ )
542
+ transcript: List[TranscriptMessage] = Field(..., title="Transcript")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.79
3
+ Version: 0.2.80
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -26,6 +26,9 @@ Requires-Dist: httpx-retries>=0.4.0
26
26
  Requires-Dist: typing-extensions>=4.0.0
27
27
  Requires-Dist: modulegraph2>=0.2.0
28
28
  Requires-Dist: cloudpickle==3.1.1
29
+ Provides-Extra: cli
30
+ Requires-Dist: typer>=0.9.0; extra == "cli"
31
+ Requires-Dist: rich>=10.0.0; extra == "cli"
29
32
  Provides-Extra: dev
30
33
  Requires-Dist: pytest>=7.0.0; extra == "dev"
31
34
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
@@ -35,6 +38,8 @@ Requires-Dist: mypy>=1.0.0; extra == "dev"
35
38
  Requires-Dist: ruff>=0.1.0; extra == "dev"
36
39
  Requires-Dist: unasync>=0.6.0; extra == "dev"
37
40
  Requires-Dist: python-dotenv>=1.1.1; extra == "dev"
41
+ Requires-Dist: typer>=0.9.0; extra == "dev"
42
+ Requires-Dist: rich>=10.0.0; extra == "dev"
38
43
  Provides-Extra: playwright
39
44
  Requires-Dist: playwright>=1.40.0; extra == "playwright"
40
45
  Dynamic: license-file
@@ -15,7 +15,7 @@ examples/export_tasks.py,sha256=Sk2Id4i5tZeVs4ZFQoAn-ABFZv7ewyv4pqtZ-Z-tIEI,3574
15
15
  examples/fetch_tasks.py,sha256=3A4F0OSUTwUddTOoDNlI-CSxeOnpl3E3zHBh0Qsw9A8,6865
16
16
  examples/gemini_example.py,sha256=qj9WDazQTYNiRHNeUg9Tjkp33lJMwbx8gDfpFe1sDQo,16180
17
17
  examples/import_tasks.py,sha256=McF5MbHenPZ7HTjQfNoHHevot0EBILMSsWtroicVT30,11619
18
- examples/iterate_verifiers.py,sha256=GCPrxz-BgEU5yv7cmiLwAo1piILIaYOQaBNON04ATMc,18457
18
+ examples/iterate_verifiers.py,sha256=XbMWyWKAYKJSufVPehG3C_JeSC7ZVXw2_s1KzIMnGVs,25435
19
19
  examples/json_tasks_example.py,sha256=CYPESGGtOo0fmsDdLidujTfsE4QlJHw7rOhyVqPJ_Ls,5329
20
20
  examples/nova_act_example.py,sha256=rH23Lp74Okf0rn8ynMdWjK2aviEf5NLPH4k_53Pyxho,831
21
21
  examples/openai_example.py,sha256=dEWERrTEP5xBiGkLkQjBQGd2NqoxX6gcW6XteBPsWFQ,8231
@@ -23,13 +23,14 @@ examples/openai_simple_example.py,sha256=HmiufucrAZne7tHq9uoEsDWlEhjNC265bQAyIGB
23
23
  examples/query_builder_example.py,sha256=-cOMfWGNifYfYEt_Ds73XpwATZvFDL6F4KTkVxdMjzg,3951
24
24
  examples/quickstart.py,sha256=1VT39IRRhemsJgxi0O0gprdpcw7HB4pYO97GAYagIcg,3788
25
25
  examples/test_cdp_logging.py,sha256=AkCwQCgOTQEI8w3v0knWK_4eXMph7L9x07wj9yIYM10,2836
26
- fleet/__init__.py,sha256=TBdp5lZtC6ltn7vuRnBNSFcArYy-2v_ONL8M2ZDzxiM,4218
26
+ fleet/__init__.py,sha256=4A84OI2wQx-uXrTMvQ86ue1Su9G3yfhTZCB78mICXZU,4218
27
27
  fleet/base.py,sha256=Z0NwvcXx43tu8j_8hmkN4FJHMl1RRYZz4I9F9xN1kkI,9609
28
- fleet/client.py,sha256=3L-woNgM-pjKgzoXhq2pgY2RHvsgUm1qBvFBkSbMWBo,45077
28
+ fleet/cli.py,sha256=YIPllz1IrdI8U4GFdxlSOmMWE02LhvWqTqnl_ORb8_8,12695
29
+ fleet/client.py,sha256=ZF7-my1yIhR7irVgvZTB-4WrNxS9mb8fJxXqq_wfR1U,50030
29
30
  fleet/config.py,sha256=n_wh9Sahu3gGE7nHJ7kqNFUH1qDiBtF4bgZq9MvIBMU,319
30
31
  fleet/exceptions.py,sha256=fUmPwWhnT8SR97lYsRq0kLHQHKtSh2eJS0VQ2caSzEI,5055
31
32
  fleet/global_client.py,sha256=frrDAFNM2ywN0JHLtlm9qbE1dQpnQJsavJpb7xSR_bU,1072
32
- fleet/models.py,sha256=7ZRUVBT0_6tiJkotWLTj52oVaeDfP4OsjjkWJxhf8CA,15409
33
+ fleet/models.py,sha256=RlaBzhKxXIYGADN-B-5zujMVP23lhhBXIB6jsJqZ_OU,20726
33
34
  fleet/tasks.py,sha256=8pEzXmgC7RslqsMC_0s6shhr_t2WGIRpTRqo-MAQjdg,20778
34
35
  fleet/types.py,sha256=L4Y82xICf1tzyCLqhLYUgEoaIIS5h9T05TyFNHSWs3s,652
35
36
  fleet/_async/__init__.py,sha256=Ars__cLbW54cO9ccZ5VFX0FJ6zKbaWXs0ByJiZpkjn8,9101
@@ -71,7 +72,7 @@ fleet/verifiers/decorator.py,sha256=RuTjjDijbicNfMSjA7HcTpKueEki5dzNOdTuHS7UoZs,
71
72
  fleet/verifiers/parse.py,sha256=qz9AfJrTbjlg-LU-lE8Ciqi7Yt2a8-cs17FdpjTLhMk,8550
72
73
  fleet/verifiers/sql_differ.py,sha256=TqTLWyK3uOyLbitT6HYzYEzuSFC39wcyhgk3rcm__k8,6525
73
74
  fleet/verifiers/verifier.py,sha256=iqGevW7dSd0J5RdRQjpu-zioy_FYAXnzMfkuB3-QmO0,14601
74
- fleet_python-0.2.79.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
75
+ fleet_python-0.2.80.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
75
76
  scripts/fix_sync_imports.py,sha256=X9fWLTpiPGkSHsjyQUDepOJkxOqw1DPj7nd8wFlFqLQ,8368
76
77
  scripts/unasync.py,sha256=vWVQxRWX8SRZO5cmzEhpvnG_REhCWXpidIGIpWmEcvI,696
77
78
  tests/__init__.py,sha256=Re1SdyxH8NfyL1kjhi7SQkGP1mYeWB-D6UALqdIMd8I,35
@@ -81,7 +82,8 @@ tests/test_instance_dispatch.py,sha256=CvU4C3LBIqsYZdEsEFfontGjyxAZfVYyXnGwxyIvX
81
82
  tests/test_sqlite_resource_dual_mode.py,sha256=Mh8jBd-xsIGDYFsOACKKK_5DXMUYlFFS7W-jaY6AjG4,8734
82
83
  tests/test_sqlite_shared_memory_behavior.py,sha256=fKx_1BmLS3b8x-9pMgjMycpnaHWY8P-2ZuXEspx6Sbw,4082
83
84
  tests/test_verifier_from_string.py,sha256=Lxi3TpFHFb-hG4-UhLKZJkqo84ax9YJY8G6beO-1erM,13581
84
- fleet_python-0.2.79.dist-info/METADATA,sha256=P2vpxM2ANXpzP5XUM2yv5vrpezwD3Hyhc6WEKa1cNmE,3524
85
- fleet_python-0.2.79.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
86
- fleet_python-0.2.79.dist-info/top_level.txt,sha256=qb1zIbtEktyhRFZdqVytwg54l64qtoZL0wjHB4bUg3c,29
87
- fleet_python-0.2.79.dist-info/RECORD,,
85
+ fleet_python-0.2.80.dist-info/METADATA,sha256=HfUrJVpZUNbk7CXH7zDocV8IqIBzY-WGuuwGPp35FeQ,3720
86
+ fleet_python-0.2.80.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
87
+ fleet_python-0.2.80.dist-info/entry_points.txt,sha256=qKIQ326cHR5WyCd16QnrW-1DpcT0YyxVRDb3IlTyzTA,39
88
+ fleet_python-0.2.80.dist-info/top_level.txt,sha256=qb1zIbtEktyhRFZdqVytwg54l64qtoZL0wjHB4bUg3c,29
89
+ fleet_python-0.2.80.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ flt = fleet.cli:main