...
This commit is contained in:
1762
aiprompts/libtmux_python.md
Normal file
1762
aiprompts/libtmux_python.md
Normal file
File diff suppressed because it is too large
Load Diff
14342
aiprompts/psutil_python.md
Normal file
14342
aiprompts/psutil_python.md
Normal file
File diff suppressed because it is too large
Load Diff
17
examples/tmuxrunner_start.py
Executable file
17
examples/tmuxrunner_start.py
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
from herolib.infra.tmuxrunner.task_runner_enhanced import TaskOrchestrator
|
||||
|
||||
def main():
|
||||
tasks_dir = sys.argv[1]
|
||||
api_port = int(sys.argv[2]) if len(sys.argv) > 2 else 8000
|
||||
orchestrator = TaskOrchestrator(tasks_dir, api_port)
|
||||
orchestrator.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python enhanced_runner.py <tasks_directory_path> [api_port]")
|
||||
sys.exit(1)
|
||||
main()
|
@@ -6,6 +6,11 @@ Author-email: Kilo Code <kilo.code@example.com>
|
||||
Requires-Python: >=3.12
|
||||
Description-Content-Type: text/markdown
|
||||
Requires-Dist: peewee
|
||||
Requires-Dist: psutil>=5.9.0
|
||||
Requires-Dist: fastapi>=0.100.0
|
||||
Requires-Dist: uvicorn>=0.23.0
|
||||
Requires-Dist: toml>=0.10.2
|
||||
Requires-Dist: libtmux>=0.25.0
|
||||
|
||||
# herolib_python
|
||||
|
||||
|
@@ -64,6 +64,7 @@ herolib/downloader/scrape_scapegraph/scrape_md.py
|
||||
herolib/downloader/scrape_scapegraph/scrape_search.py
|
||||
herolib/downloader/scrape_scapegraph/scrape_with_local_llm.py
|
||||
herolib/downloader/scrape_scapegraph/scrape_with_local_llm_search.py
|
||||
herolib/infra/tmuxrunner/enhanced_runner.py
|
||||
herolib/tools/__init__.py
|
||||
herolib/tools/extensions.py
|
||||
herolib/tools/gitscanner.py
|
||||
|
@@ -1 +1,6 @@
|
||||
peewee
|
||||
psutil>=5.9.0
|
||||
fastapi>=0.100.0
|
||||
uvicorn>=0.23.0
|
||||
toml>=0.10.2
|
||||
libtmux>=0.25.0
|
||||
|
0
herolib/infra/tmuxrunner/instructions.md
Normal file
0
herolib/infra/tmuxrunner/instructions.md
Normal file
86
herolib/infra/tmuxrunner/model.py
Normal file
86
herolib/infra/tmuxrunner/model.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import sys
|
||||
import toml
|
||||
import libtmux
|
||||
from libtmux.pane import Pane
|
||||
from libtmux.window import Window
|
||||
from libtmux.session import Session
|
||||
import psutil
|
||||
from typing import Dict, List, Optional, Any, Set, Tuple
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
import threading
|
||||
|
||||
# Configuration
|
||||
WAITING_MESSAGE = "WAITING FOR JOBS"
|
||||
HPY_SH_PATH = "/root/heromonkey/functions/hpy.sh" # Path to hpy.sh
|
||||
|
||||
@dataclass
|
||||
class ProcessMetrics:
|
||||
"""Metrics for a running process and its children."""
|
||||
cpu_percent: float = 0.0
|
||||
memory_rss: int = 0 # Resident Set Size in bytes
|
||||
memory_vms: int = 0 # Virtual Memory Size in bytes
|
||||
memory_percent: float = 0.0
|
||||
num_threads: int = 0
|
||||
num_children: int = 0
|
||||
children_cpu_percent: float = 0.0
|
||||
children_memory_rss: int = 0
|
||||
last_updated: str = ""
|
||||
|
||||
@dataclass
|
||||
class TaskStatus:
|
||||
"""Status of an individual task (script)."""
|
||||
script_path: str
|
||||
script_name: str
|
||||
state: str = "PENDING" # PENDING, WAITING, RUNNING, DONE, ERROR, CRASHED, TIMED_OUT
|
||||
start_time: Optional[str] = None
|
||||
end_time: Optional[str] = None
|
||||
duration_seconds: float = 0.0
|
||||
exit_code: Optional[int] = None
|
||||
error_message: Optional[str] = None
|
||||
pane_id: Optional[str] = None
|
||||
process_metrics: ProcessMetrics = field(default_factory=ProcessMetrics)
|
||||
|
||||
@dataclass
|
||||
class DirectoryStatus:
|
||||
"""Status of a directory containing tasks."""
|
||||
directory_num: int
|
||||
directory_path: str
|
||||
state: str = "PENDING" # PENDING, RUNNING, DONE, ERROR, TIMED_OUT
|
||||
timeout: int = 600
|
||||
start_time: Optional[str] = None
|
||||
end_time: Optional[str] = None
|
||||
duration_seconds: float = 0.0
|
||||
tasks: List[TaskStatus] = field(default_factory=list)
|
||||
window_name: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class DAGStructure:
|
||||
"""Complete DAG structure for the task run."""
|
||||
run_name: str
|
||||
run_id: str
|
||||
state: str = "INITIALIZING" # INITIALIZING, RUNNING, COMPLETED, FAILED
|
||||
start_time: str = ""
|
||||
end_time: Optional[str] = None
|
||||
duration_seconds: float = 0.0
|
||||
total_directories: int = 0
|
||||
completed_directories: int = 0
|
||||
failed_directories: int = 0
|
||||
directories: List[DirectoryStatus] = field(default_factory=list)
|
||||
last_updated: str = ""
|
||||
|
||||
class MetaData:
|
||||
"""Class to hold metadata for a task directory."""
|
||||
def __init__(self, timeout: int = 600): # Default timeout to 10 minutes (600 seconds)
|
||||
self.timeout = timeout
|
||||
# Add more attributes here in the future
|
106
herolib/infra/tmuxrunner/process_monitor.py
Normal file
106
herolib/infra/tmuxrunner/process_monitor.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import sys
|
||||
import toml
|
||||
import libtmux
|
||||
from libtmux.pane import Pane
|
||||
from libtmux.window import Window
|
||||
from libtmux.session import Session
|
||||
import psutil
|
||||
from typing import Dict, List, Optional, Any, Set, Tuple
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
import threading
|
||||
|
||||
|
||||
class ProcessMonitor:
|
||||
"""Monitor processes running in tmux panes using psutil."""
|
||||
|
||||
@staticmethod
|
||||
def get_pane_process_tree(pane: Pane) -> Tuple[Optional[psutil.Process], List[psutil.Process]]:
|
||||
"""Get the main process and all child processes for a tmux pane."""
|
||||
try:
|
||||
pane_pid = pane.pane_pid
|
||||
if pane_pid is None:
|
||||
return None, []
|
||||
|
||||
# Get the main process
|
||||
try:
|
||||
main_process = psutil.Process(int(pane_pid))
|
||||
except (psutil.NoSuchProcess, ValueError):
|
||||
return None, []
|
||||
|
||||
# Get all children recursively
|
||||
children = []
|
||||
try:
|
||||
children = main_process.children(recursive=True)
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
return main_process, children
|
||||
except Exception as e:
|
||||
print(f"Error getting process tree: {e}")
|
||||
return None, []
|
||||
|
||||
@staticmethod
|
||||
def get_process_metrics(pane: Pane) -> ProcessMetrics:
|
||||
"""Get CPU and memory metrics for all processes in a pane."""
|
||||
metrics = ProcessMetrics()
|
||||
metrics.last_updated = datetime.now().isoformat()
|
||||
|
||||
main_proc, children = ProcessMonitor.get_pane_process_tree(pane)
|
||||
|
||||
if main_proc is None:
|
||||
return metrics
|
||||
|
||||
try:
|
||||
# Get main process metrics
|
||||
if main_proc.is_running():
|
||||
metrics.cpu_percent = main_proc.cpu_percent(interval=0.1)
|
||||
mem_info = main_proc.memory_info()
|
||||
metrics.memory_rss = mem_info.rss
|
||||
metrics.memory_vms = mem_info.vms
|
||||
metrics.memory_percent = main_proc.memory_percent()
|
||||
metrics.num_threads = main_proc.num_threads()
|
||||
|
||||
# Get children metrics
|
||||
metrics.num_children = len(children)
|
||||
for child in children:
|
||||
try:
|
||||
if child.is_running():
|
||||
metrics.children_cpu_percent += child.cpu_percent(interval=0.1)
|
||||
child_mem = child.memory_info()
|
||||
metrics.children_memory_rss += child_mem.rss
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
|
||||
print(f"Error getting process metrics: {e}")
|
||||
|
||||
return metrics
|
||||
|
||||
@staticmethod
|
||||
def is_process_running_command(pane: Pane, command_pattern: str) -> bool:
|
||||
"""Check if a specific command is running in the pane."""
|
||||
main_proc, children = ProcessMonitor.get_pane_process_tree(pane)
|
||||
|
||||
all_processes = [main_proc] + children if main_proc else children
|
||||
|
||||
for proc in all_processes:
|
||||
try:
|
||||
if proc and proc.is_running():
|
||||
cmdline = " ".join(proc.cmdline())
|
||||
if command_pattern in cmdline:
|
||||
return True
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
|
||||
return False
|
594
herolib/infra/tmuxrunner/task_runner.py
Normal file
594
herolib/infra/tmuxrunner/task_runner.py
Normal file
@@ -0,0 +1,594 @@
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import sys
|
||||
import toml
|
||||
import libtmux
|
||||
from libtmux.pane import Pane
|
||||
from libtmux.window import Window
|
||||
from libtmux.session import Session
|
||||
import psutil
|
||||
from typing import Dict, List, Optional, Any, Set, Tuple
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
import threading
|
||||
|
||||
# Configuration
|
||||
WAITING_MESSAGE = "WAITING FOR JOBS"
|
||||
HPY_SH_PATH = "/root/heromonkey/functions/hpy.sh" # Path to hpy.sh
|
||||
|
||||
|
||||
class TaskRunner:
|
||||
def __init__(self, tasks_root_dir: str):
|
||||
self.tasks_root_dir = tasks_root_dir
|
||||
self.run_name = os.path.basename(os.path.abspath(tasks_root_dir)) # Derive run_name
|
||||
self.session = self._get_current_tmux_session()
|
||||
self.all_tasks_with_meta = self._get_sorted_tasks_with_meta(tasks_root_dir)
|
||||
self.window_panes = {} # {window_idx: [pane1, pane2, ...]}
|
||||
self.run_id = str(uuid.uuid4())
|
||||
self.dag = self._initialize_dag()
|
||||
self.dag_file_path = Path(tasks_root_dir) / ".dag.toml"
|
||||
self.process_monitor = ProcessMonitor()
|
||||
self._save_dag()
|
||||
|
||||
def _initialize_dag(self) -> DAGStructure:
|
||||
"""Initialize the DAG structure."""
|
||||
dag = DAGStructure(
|
||||
run_name=self.run_name,
|
||||
run_id=self.run_id,
|
||||
state="INITIALIZING",
|
||||
start_time=datetime.now().isoformat(),
|
||||
total_directories=len(self.all_tasks_with_meta)
|
||||
)
|
||||
|
||||
# Create directory entries
|
||||
for dir_num, (scripts, metadata) in self.all_tasks_with_meta.items():
|
||||
dir_status = DirectoryStatus(
|
||||
directory_num=dir_num,
|
||||
directory_path=os.path.dirname(scripts[0]) if scripts else "",
|
||||
timeout=metadata.timeout,
|
||||
window_name=f"{self.run_name}_{dir_num}"
|
||||
)
|
||||
|
||||
# Create task entries
|
||||
for script_path in scripts:
|
||||
task = TaskStatus(
|
||||
script_path=script_path,
|
||||
script_name=os.path.basename(script_path)
|
||||
)
|
||||
dir_status.tasks.append(task)
|
||||
|
||||
dag.directories.append(dir_status)
|
||||
|
||||
return dag
|
||||
|
||||
def _save_dag(self):
|
||||
"""Save the DAG structure to a TOML file."""
|
||||
try:
|
||||
dag_dict = asdict(self.dag)
|
||||
with open(self.dag_file_path, 'w') as f:
|
||||
toml.dump(dag_dict, f)
|
||||
except Exception as e:
|
||||
print(f"Error saving DAG: {e}")
|
||||
|
||||
def _update_task_state(self, dir_idx: int, task_idx: int,
|
||||
state: str, error_message: Optional[str] = None):
|
||||
"""Update task state and save DAG."""
|
||||
task = self.dag.directories[dir_idx].tasks[task_idx]
|
||||
old_state = task.state
|
||||
task.state = state
|
||||
|
||||
if state == "RUNNING" and old_state != "RUNNING":
|
||||
task.start_time = datetime.now().isoformat()
|
||||
elif state in ["DONE", "ERROR", "CRASHED", "TIMED_OUT"]:
|
||||
task.end_time = datetime.now().isoformat()
|
||||
if task.start_time:
|
||||
start = datetime.fromisoformat(task.start_time)
|
||||
end = datetime.fromisoformat(task.end_time)
|
||||
task.duration_seconds = (end - start).total_seconds()
|
||||
|
||||
if error_message:
|
||||
task.error_message = error_message
|
||||
|
||||
self.dag.last_updated = datetime.now().isoformat()
|
||||
self._save_dag()
|
||||
|
||||
def _update_directory_state(self, dir_idx: int, state: str):
|
||||
"""Update directory state and save DAG."""
|
||||
directory = self.dag.directories[dir_idx]
|
||||
old_state = directory.state
|
||||
directory.state = state
|
||||
|
||||
if state == "RUNNING" and old_state != "RUNNING":
|
||||
directory.start_time = datetime.now().isoformat()
|
||||
elif state in ["DONE", "ERROR", "TIMED_OUT"]:
|
||||
directory.end_time = datetime.now().isoformat()
|
||||
if directory.start_time:
|
||||
start = datetime.fromisoformat(directory.start_time)
|
||||
end = datetime.fromisoformat(directory.end_time)
|
||||
directory.duration_seconds = (end - start).total_seconds()
|
||||
|
||||
if state == "DONE":
|
||||
self.dag.completed_directories += 1
|
||||
else:
|
||||
self.dag.failed_directories += 1
|
||||
|
||||
self._save_dag()
|
||||
|
||||
def _check_task_status(self, script_path: str, pane: Pane) -> Tuple[str, Optional[str]]:
|
||||
"""
|
||||
Comprehensive task status checking.
|
||||
Returns: (state, error_message)
|
||||
"""
|
||||
script_basename = os.path.basename(script_path)
|
||||
done_file = f"{script_path}.done"
|
||||
error_file = f"{script_path}.error"
|
||||
ok_file = f"{script_path}.ok"
|
||||
|
||||
# Check file markers
|
||||
if os.path.exists(done_file) or os.path.exists(ok_file):
|
||||
# Create .ok file if it doesn't exist
|
||||
if not os.path.exists(ok_file):
|
||||
Path(ok_file).touch()
|
||||
return "DONE", None
|
||||
|
||||
if os.path.exists(error_file):
|
||||
error_msg = None
|
||||
try:
|
||||
with open(error_file, 'r') as f:
|
||||
error_msg = f.read().strip()
|
||||
except:
|
||||
error_msg = "Unknown error"
|
||||
return "ERROR", error_msg
|
||||
|
||||
# Check if hpy command is running
|
||||
if self.process_monitor.is_process_running_command(pane, f"hpy {script_basename}"):
|
||||
return "RUNNING", None
|
||||
|
||||
# Check if pane has any running process
|
||||
if self._is_pane_running(pane):
|
||||
# Might be setting up or running something else
|
||||
return "RUNNING", None
|
||||
|
||||
# If we get here, the process finished without markers
|
||||
# This is likely a crash
|
||||
error_msg = f"Process terminated without completion marker"
|
||||
# Create error file
|
||||
with open(error_file, 'w') as f:
|
||||
f.write(error_msg)
|
||||
return "CRASHED", error_msg
|
||||
|
||||
def _monitor_directory_tasks(self, dir_idx: int, timeout: int) -> bool:
|
||||
"""
|
||||
Monitor tasks in a directory with comprehensive status checking.
|
||||
Returns: True if all tasks completed successfully, False otherwise.
|
||||
"""
|
||||
directory = self.dag.directories[dir_idx]
|
||||
scripts, metadata = self.all_tasks_with_meta[directory.directory_num]
|
||||
panes = self.window_panes[dir_idx]
|
||||
|
||||
self._update_directory_state(dir_idx, "RUNNING")
|
||||
|
||||
start_time = time.time()
|
||||
all_success = True
|
||||
|
||||
while True:
|
||||
all_finished = True
|
||||
has_errors = False
|
||||
|
||||
for task_idx, (script_path, pane) in enumerate(zip(scripts, panes)):
|
||||
task = directory.tasks[task_idx]
|
||||
|
||||
# Get process metrics if running
|
||||
if task.state == "RUNNING":
|
||||
metrics = self.process_monitor.get_process_metrics(pane)
|
||||
task.process_metrics = metrics
|
||||
|
||||
# Check task status
|
||||
new_state, error_msg = self._check_task_status(script_path, pane)
|
||||
|
||||
if new_state != task.state:
|
||||
self._update_task_state(dir_idx, task_idx, new_state, error_msg)
|
||||
print(f" Task {task.script_name}: {task.state}")
|
||||
|
||||
if new_state == "RUNNING":
|
||||
all_finished = False
|
||||
elif new_state in ["ERROR", "CRASHED", "TIMED_OUT"]:
|
||||
has_errors = True
|
||||
all_success = False
|
||||
|
||||
# Save DAG periodically
|
||||
self._save_dag()
|
||||
|
||||
if all_finished:
|
||||
if has_errors:
|
||||
self._update_directory_state(dir_idx, "ERROR")
|
||||
else:
|
||||
self._update_directory_state(dir_idx, "DONE")
|
||||
break
|
||||
|
||||
# Check timeout
|
||||
elapsed = time.time() - start_time
|
||||
if elapsed > timeout:
|
||||
print(f" Directory {directory.directory_num} timed out!")
|
||||
for task_idx, task in enumerate(directory.tasks):
|
||||
if task.state == "RUNNING":
|
||||
self._update_task_state(dir_idx, task_idx, "TIMED_OUT")
|
||||
panes[task_idx].send_keys("C-c", literal=True)
|
||||
self._update_directory_state(dir_idx, "TIMED_OUT")
|
||||
all_success = False
|
||||
break
|
||||
|
||||
time.sleep(2) # Check every 2 seconds
|
||||
|
||||
return all_success
|
||||
|
||||
def run(self):
|
||||
"""Enhanced run method with DAG tracking."""
|
||||
print(f"Starting enhanced task orchestration for '{self.run_name}'")
|
||||
print(f"Run ID: {self.run_id}")
|
||||
print(f"DAG file: {self.dag_file_path}")
|
||||
|
||||
self.dag.state = "RUNNING"
|
||||
self._save_dag()
|
||||
|
||||
# Initialize windows and panes (similar to original)
|
||||
self._setup_windows_and_panes()
|
||||
|
||||
# Process directories sequentially
|
||||
overall_success = True
|
||||
for dir_idx in range(len(self.dag.directories)):
|
||||
directory = self.dag.directories[dir_idx]
|
||||
print(f"\n--- Processing Directory {directory.directory_num} ---")
|
||||
|
||||
# Start tasks if not the first directory
|
||||
if dir_idx > 0:
|
||||
self._start_directory_tasks(dir_idx)
|
||||
|
||||
# Monitor tasks
|
||||
success = self._monitor_directory_tasks(
|
||||
dir_idx,
|
||||
directory.timeout
|
||||
)
|
||||
|
||||
if not success:
|
||||
overall_success = False
|
||||
|
||||
# Update final DAG state
|
||||
self.dag.state = "COMPLETED" if overall_success else "FAILED"
|
||||
self.dag.end_time = datetime.now().isoformat()
|
||||
if self.dag.start_time:
|
||||
start = datetime.fromisoformat(self.dag.start_time)
|
||||
end = datetime.fromisoformat(self.dag.end_time)
|
||||
self.dag.duration_seconds = (end - start).total_seconds()
|
||||
self._save_dag()
|
||||
|
||||
print(f"\nTask orchestration completed: {self.dag.state}")
|
||||
print(f"Total duration: {self.dag.duration_seconds:.2f} seconds")
|
||||
|
||||
def reset(self):
|
||||
"""Kills all processes and panes inside task windows, removes windows, and deletes .done/.error/.ok files."""
|
||||
print(f"\n--- Resetting run '{self.run_name}' ---")
|
||||
self.cleanup() # First, kill all associated tmux windows
|
||||
|
||||
# Then, remove all .done, .error, and .ok files
|
||||
print(" Removing .done, .error, and .ok files...")
|
||||
for dir_num, (scripts, metadata) in self.all_tasks_with_meta.items():
|
||||
for script_path in scripts:
|
||||
done_file = f"{script_path}.done"
|
||||
error_file = f"{script_path}.error"
|
||||
ok_file = f"{script_path}.ok"
|
||||
if os.path.exists(done_file):
|
||||
os.remove(done_file)
|
||||
print(f" Removed: {done_file}")
|
||||
if os.path.exists(error_file):
|
||||
os.remove(error_file)
|
||||
print(f" Removed: {error_file}")
|
||||
if os.path.exists(ok_file):
|
||||
os.remove(ok_file)
|
||||
print(f" Removed: {ok_file}")
|
||||
|
||||
# Also remove the .dag.toml file if it exists
|
||||
if hasattr(self, 'dag_file_path') and self.dag_file_path.exists():
|
||||
os.remove(self.dag_file_path)
|
||||
print(f" Removed: {self.dag_file_path}")
|
||||
print("Reset complete.")
|
||||
|
||||
|
||||
|
||||
def _get_sorted_tasks_with_meta(self, tasks_root):
|
||||
"""
|
||||
Reads all scripts and .meta.toml from the tasks_root, sorts them by directory,
|
||||
and then by script name within each directory.
|
||||
Returns a dictionary where keys are directory numbers (e.g., 1, 2)
|
||||
and values are tuples of (list_of_full_script_paths, MetaData_object).
|
||||
"""
|
||||
tasks_with_meta = {}
|
||||
for dirpath, dirnames, filenames in os.walk(tasks_root):
|
||||
if dirpath == tasks_root:
|
||||
dirnames[:] = sorted([d for d in dirnames if d.isdigit()], key=int)
|
||||
|
||||
relative_path = os.path.relpath(dirpath, tasks_root)
|
||||
if relative_path != "." and relative_path.isdigit():
|
||||
dir_num = int(relative_path)
|
||||
scripts = sorted([os.path.join(dirpath, f) for f in filenames if f.endswith(".sh")])
|
||||
|
||||
metadata_file = os.path.join(dirpath, ".meta.toml")
|
||||
metadata = MetaData() # Default metadata
|
||||
if os.path.exists(metadata_file):
|
||||
try:
|
||||
with open(metadata_file, 'r') as f:
|
||||
meta_data_dict = toml.load(f)
|
||||
if 'timeout' in meta_data_dict:
|
||||
metadata.timeout = int(meta_data_dict['timeout'])
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not read or parse .meta.toml for directory {dir_num}: {e}")
|
||||
|
||||
if scripts:
|
||||
tasks_with_meta[dir_num] = (scripts, metadata)
|
||||
|
||||
sorted_tasks_with_meta = dict(sorted(tasks_with_meta.items()))
|
||||
return sorted_tasks_with_meta
|
||||
|
||||
def _get_current_tmux_session(self) -> Session:
|
||||
"""Gets the current tmux session based on TMUX environment variable."""
|
||||
server = libtmux.Server()
|
||||
tmux_env = os.environ.get('TMUX')
|
||||
|
||||
if not tmux_env:
|
||||
raise Exception("Not running inside a tmux session. The 'TMUX' environment variable is not set.")
|
||||
|
||||
try:
|
||||
# TMUX variable format: /tmp/tmux-1000/default,12345,0
|
||||
# The last part '0' is the session index.
|
||||
match = re.search(r',(\d+)$', tmux_env)
|
||||
if not match:
|
||||
raise Exception(f"Could not parse session index from TMUX environment variable: {tmux_env}")
|
||||
|
||||
session_index_from_env = match.group(1)
|
||||
|
||||
found_session = None
|
||||
for s in server.sessions:
|
||||
if s.session_id == f"${session_index_from_env}":
|
||||
found_session = s
|
||||
break
|
||||
|
||||
if not found_session:
|
||||
raise Exception(f"Could not find tmux session with ID: ${session_index_from_env}")
|
||||
|
||||
print(f"Attached to current tmux session: '{found_session.name}' via TMUX env var.")
|
||||
return found_session
|
||||
except Exception as e:
|
||||
raise Exception(f"Error getting current tmux session: {e}")
|
||||
|
||||
def _create_tmux_window(self, window_name: str) -> Window:
|
||||
"""Creates a new tmux window."""
|
||||
window = self.session.new_window(attach=False, window_name=window_name)
|
||||
print(f" Tmux window '{window_name}' created.")
|
||||
return window
|
||||
|
||||
def _create_tmux_pane(self, window: Window, pane_index: int, command: str) -> Pane:
|
||||
"""Creates a tmux pane and sends a command."""
|
||||
if pane_index == 0:
|
||||
pane = window.active_pane
|
||||
pane.send_keys("clear", enter=True)
|
||||
else:
|
||||
pane = window.split(attach=False)
|
||||
|
||||
pane.send_keys(command, enter=True)
|
||||
print(f" Pane {pane_index}: Command sent: '{command}'")
|
||||
return pane
|
||||
|
||||
def _is_pane_running(self, pane: Pane) -> bool:
|
||||
"""Checks if a tmux pane is still running a process."""
|
||||
try:
|
||||
pane_pid = pane.pane_pid
|
||||
if pane_pid is not None:
|
||||
try:
|
||||
pid_int = int(pane_pid)
|
||||
if pid_int > 0:
|
||||
os.kill(pid_int, 0)
|
||||
return True
|
||||
except (ValueError, OSError):
|
||||
return False
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error checking pane status for {pane.window_name}:{pane.pane_index}: {e}")
|
||||
return False
|
||||
|
||||
def _setup_windows_and_panes(self):
|
||||
"""Initial setup of tmux windows and panes for all tasks."""
|
||||
all_dir_nums = sorted(self.all_tasks_with_meta.keys())
|
||||
print("\n--- Initial Tmux Setup: Creating windows and panes ---")
|
||||
for window_idx, dir_num in enumerate(all_dir_nums):
|
||||
scripts, metadata = self.all_tasks_with_meta[dir_num]
|
||||
window_name = f"{self.run_name}_{dir_num}"
|
||||
window = self._create_tmux_window(window_name)
|
||||
self.window_panes[window_idx] = []
|
||||
|
||||
for pane_idx, script_path in enumerate(scripts):
|
||||
script_dir = os.path.dirname(script_path)
|
||||
script_basename = os.path.basename(script_path)
|
||||
|
||||
if window_idx == 0:
|
||||
# Send cd command first, then the hpy command
|
||||
pane = self._create_tmux_pane(window, pane_idx, f"cd {script_dir}")
|
||||
pane.send_keys(f"source {HPY_SH_PATH} && hpy {script_basename}; echo \"Script {script_basename} finished.\"", enter=True)
|
||||
print(f" Pane {pane_idx}: Command sent: 'cd {script_dir}' and 'source {HPY_SH_PATH} && hpy {script_basename}'")
|
||||
else:
|
||||
command = f"echo '{WAITING_MESSAGE} for {script_basename}'"
|
||||
pane = self._create_tmux_pane(window, pane_idx, command)
|
||||
self.window_panes[window_idx].append(pane)
|
||||
|
||||
if window_idx == 0:
|
||||
print(f" Window '{window_name}' (Directory {dir_num}) tasks started.")
|
||||
else:
|
||||
print(f" Window '{window_name}' (Directory {dir_num}) panes set to '{WAITING_MESSAGE}'.")
|
||||
|
||||
def _start_directory_tasks(self, dir_idx: int):
|
||||
"""Starts tasks in a specific directory (window)."""
|
||||
directory = self.dag.directories[dir_idx]
|
||||
scripts, metadata = self.all_tasks_with_meta[directory.directory_num]
|
||||
panes_in_current_window = self.window_panes[dir_idx]
|
||||
|
||||
print(f"\n--- Activating tasks in window '{directory.window_name}' (Directory {directory.directory_num}) ---")
|
||||
for pane_idx, script_path in enumerate(scripts):
|
||||
script_dir = os.path.dirname(script_path)
|
||||
script_basename = os.path.basename(script_path)
|
||||
|
||||
pane = panes_in_current_window[pane_idx]
|
||||
pane.send_keys("C-c", literal=True) # Clear any previous command/output
|
||||
|
||||
# Send cd command first, then the hpy command
|
||||
pane.send_keys(f"cd {script_dir}", enter=True)
|
||||
pane.send_keys(f"source {HPY_SH_PATH} && hpy {script_basename}; echo \"Script {script_basename} finished.\"", enter=True)
|
||||
print(f" Pane {pane_idx}: Command sent: 'cd {script_dir}' and 'source {HPY_SH_PATH} && hpy {script_basename}'")
|
||||
|
||||
def run(self):
|
||||
print(f"Starting task orchestration for run '{self.run_name}' from {self.tasks_root_dir}")
|
||||
|
||||
if not self.all_tasks_with_meta:
|
||||
print("No tasks found to execute.")
|
||||
return
|
||||
|
||||
print("Detected tasks:")
|
||||
for dir_num, (scripts, metadata) in self.all_tasks_with_meta.items():
|
||||
print(f" Directory {dir_num} (Timeout: {metadata.timeout}s):")
|
||||
for script in scripts:
|
||||
print(f" - {script}")
|
||||
|
||||
all_dir_nums = sorted(self.all_tasks_with_meta.keys())
|
||||
|
||||
# Phase 1: Initial setup - create all windows and panes,
|
||||
# execute first window's tasks, set others to WAITING.
|
||||
self._setup_windows_and_panes()
|
||||
|
||||
# Phase 2: Sequential execution and monitoring
|
||||
print("\n--- Starting Sequential Task Execution ---")
|
||||
for window_idx, dir_num in enumerate(all_dir_nums):
|
||||
scripts, metadata = self.all_tasks_with_meta[dir_num]
|
||||
window_name = f"{self.run_name}_{dir_num}"
|
||||
panes_in_current_window = self.window_panes[window_idx]
|
||||
|
||||
if window_idx > 0:
|
||||
self._start_directory_tasks(window_idx)
|
||||
|
||||
print(f" Monitoring tasks in window '{window_name}' (Timeout: {metadata.timeout}s)...")
|
||||
start_time = time.time()
|
||||
script_states = {os.path.basename(s): "INITIAL" for s in scripts} # Track state for each script
|
||||
|
||||
while True:
|
||||
all_tasks_finished_or_failed = True
|
||||
current_status_report = []
|
||||
|
||||
for pane_idx, script_path in enumerate(scripts):
|
||||
script_basename = os.path.basename(script_path)
|
||||
pane = panes_in_current_window[pane_idx]
|
||||
done_file = f"{script_path}.done"
|
||||
error_file = f"{script_path}.error"
|
||||
ok_file = f"{script_path}.ok" # Added .ok file check
|
||||
|
||||
current_script_state = script_states[script_basename]
|
||||
new_script_state = current_script_state
|
||||
|
||||
if os.path.exists(done_file) or os.path.exists(ok_file): # Check for .ok file
|
||||
new_script_state = "DONE"
|
||||
elif os.path.exists(error_file):
|
||||
new_script_state = "ERROR"
|
||||
elif self._is_pane_running(pane):
|
||||
new_script_state = "RUNNING"
|
||||
all_tasks_finished_or_failed = False
|
||||
elif current_script_state == "RUNNING":
|
||||
# If it was running but now isn't, and no done/error file, it crashed
|
||||
new_script_state = "CRASHED"
|
||||
|
||||
if new_script_state != current_script_state:
|
||||
script_states[script_basename] = new_script_state
|
||||
current_status_report.append(f" - {script_basename}: {new_script_state}")
|
||||
|
||||
if current_status_report:
|
||||
print(f" Status update for window '{window_name}' (Elapsed: {int(time.time() - start_time)}s):")
|
||||
for report_line in current_status_report:
|
||||
print(report_line)
|
||||
|
||||
if all_tasks_finished_or_failed:
|
||||
print(f" All tasks in window '{window_name}' completed or failed.")
|
||||
break
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
if elapsed_time > metadata.timeout:
|
||||
print(f" ERROR: Tasks in window '{window_name}' timed out after {metadata.timeout}s.")
|
||||
for pane_idx, script_path in enumerate(scripts):
|
||||
script_basename = os.path.basename(script_path)
|
||||
pane = panes_in_current_window[pane_idx]
|
||||
if script_states[script_basename] == "RUNNING":
|
||||
script_states[script_basename] = "TIMED_OUT"
|
||||
print(f" - {script_basename}: TIMED_OUT (Killing pane)")
|
||||
pane.send_keys("C-c", literal=True)
|
||||
time.sleep(1)
|
||||
break
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
print("\nAll task directories processed. You can attach to the tmux session to review:")
|
||||
print(f"tmux attach -t {self.session.name}")
|
||||
|
||||
def cleanup(self):
|
||||
"""Removes all tmux windows created by this run."""
|
||||
print(f"\n--- Cleaning up tmux windows for run '{self.run_name}' ---")
|
||||
print(f" Current session name: '{self.session.name}'")
|
||||
all_session_windows = [w.name for w in self.session.windows if w.name]
|
||||
print(f" All windows in current session: {all_session_windows}")
|
||||
|
||||
windows_to_kill = []
|
||||
expected_prefix = f"{self.run_name}_"
|
||||
print(f" Looking for windows starting with prefix: '{expected_prefix}'")
|
||||
|
||||
for window in self.session.windows:
|
||||
if window.name and window.name.startswith(expected_prefix):
|
||||
windows_to_kill.append(window)
|
||||
|
||||
if not windows_to_kill:
|
||||
print(f" No windows found to kill with prefix '{expected_prefix}'.")
|
||||
print("Cleanup complete.")
|
||||
return
|
||||
|
||||
print(f" Identified {len(windows_to_kill)} windows to kill: {[w.name for w in windows_to_kill]}")
|
||||
for window in windows_to_kill:
|
||||
try:
|
||||
window.kill()
|
||||
print(f" Killed window: '{window.name}'")
|
||||
except Exception as e:
|
||||
print(f" Error killing window '{window.name}': {e}")
|
||||
print("Cleanup complete.")
|
||||
|
||||
def reset(self):
|
||||
"""Kills all processes and panes inside task windows, removes windows, and deletes .done/.error files."""
|
||||
print(f"\n--- Resetting run '{self.run_name}' ---")
|
||||
self.cleanup() # First, kill all associated tmux windows
|
||||
|
||||
# Then, remove all .done and .error files
|
||||
print(" Removing .done and .error files...")
|
||||
for dir_num, (scripts, metadata) in self.all_tasks_with_meta.items():
|
||||
for script_path in scripts:
|
||||
done_file = f"{script_path}.done"
|
||||
error_file = f"{script_path}.error"
|
||||
ok_file = f"{script_path}.ok" # Added .ok file to be removed
|
||||
if os.path.exists(done_file):
|
||||
os.remove(done_file)
|
||||
print(f" Removed: {done_file}")
|
||||
if os.path.exists(error_file):
|
||||
os.remove(error_file)
|
||||
print(f" Removed: {error_file}")
|
||||
if os.path.exists(ok_file): # Remove .ok file
|
||||
os.remove(ok_file)
|
||||
print(f" Removed: {ok_file}")
|
||||
print("Reset complete.")
|
||||
|
176
herolib/infra/tmuxrunner/task_runner_api.py
Normal file
176
herolib/infra/tmuxrunner/task_runner_api.py
Normal file
@@ -0,0 +1,176 @@
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import sys
|
||||
import toml
|
||||
import libtmux
|
||||
from libtmux.pane import Pane
|
||||
from libtmux.window import Window
|
||||
from libtmux.session import Session
|
||||
import psutil
|
||||
from typing import Dict, List, Optional, Any, Set, Tuple
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
import threading
|
||||
|
||||
|
||||
|
||||
class TaskRunnerAPI:
|
||||
"""FastAPI interface for the task runner."""
|
||||
|
||||
def __init__(self, runner: EnhancedTaskRunner):
|
||||
self.runner = runner
|
||||
self.app = FastAPI(title="Task Runner API", version="1.0.0")
|
||||
|
||||
# Add CORS middleware
|
||||
self.app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
self._setup_routes()
|
||||
|
||||
def _setup_routes(self):
|
||||
"""Setup API routes."""
|
||||
|
||||
@self.app.get("/")
|
||||
async def root():
|
||||
"""Get API information."""
|
||||
return {
|
||||
"name": "Task Runner API",
|
||||
"version": "1.0.0",
|
||||
"run_id": self.runner.run_id,
|
||||
"run_name": self.runner.run_name
|
||||
}
|
||||
|
||||
@self.app.get("/status")
|
||||
async def get_status():
|
||||
"""Get current run status."""
|
||||
return {
|
||||
"run_id": self.runner.run_id,
|
||||
"run_name": self.runner.run_name,
|
||||
"state": self.runner.dag.state,
|
||||
"start_time": self.runner.dag.start_time,
|
||||
"end_time": self.runner.dag.end_time,
|
||||
"duration_seconds": self.runner.dag.duration_seconds,
|
||||
"total_directories": self.runner.dag.total_directories,
|
||||
"completed_directories": self.runner.dag.completed_directories,
|
||||
"failed_directories": self.runner.dag.failed_directories
|
||||
}
|
||||
|
||||
@self.app.get("/directories")
|
||||
async def get_directories():
|
||||
"""Get all directory statuses."""
|
||||
return [
|
||||
{
|
||||
"directory_num": d.directory_num,
|
||||
"directory_path": d.directory_path,
|
||||
"state": d.state,
|
||||
"timeout": d.timeout,
|
||||
"start_time": d.start_time,
|
||||
"end_time": d.end_time,
|
||||
"duration_seconds": d.duration_seconds,
|
||||
"task_count": len(d.tasks),
|
||||
"tasks_done": sum(1 for t in d.tasks if t.state == "DONE"),
|
||||
"tasks_error": sum(1 for t in d.tasks if t.state in ["ERROR", "CRASHED", "TIMED_OUT"])
|
||||
}
|
||||
for d in self.runner.dag.directories
|
||||
]
|
||||
|
||||
@self.app.get("/directories/{dir_num}/tasks")
|
||||
async def get_directory_tasks(dir_num: int):
|
||||
"""Get tasks for a specific directory."""
|
||||
for d in self.runner.dag.directories:
|
||||
if d.directory_num == dir_num:
|
||||
return d.tasks
|
||||
raise HTTPException(status_code=404, detail="Directory not found")
|
||||
|
||||
@self.app.get("/tasks/{dir_num}/{task_name}")
|
||||
async def get_task_details(dir_num: int, task_name: str):
|
||||
"""Get detailed information about a specific task."""
|
||||
for d in self.runner.dag.directories:
|
||||
if d.directory_num == dir_num:
|
||||
for t in d.tasks:
|
||||
if t.script_name == task_name:
|
||||
return t
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
@self.app.get("/metrics")
|
||||
async def get_metrics():
|
||||
"""Get current process metrics for all running tasks."""
|
||||
metrics = []
|
||||
for d in self.runner.dag.directories:
|
||||
for t in d.tasks:
|
||||
if t.state == "RUNNING":
|
||||
metrics.append({
|
||||
"directory": d.directory_num,
|
||||
"task": t.script_name,
|
||||
"cpu_percent": t.process_metrics.cpu_percent,
|
||||
"memory_rss_mb": t.process_metrics.memory_rss / (1024 * 1024),
|
||||
"memory_percent": t.process_metrics.memory_percent,
|
||||
"num_threads": t.process_metrics.num_threads,
|
||||
"num_children": t.process_metrics.num_children
|
||||
})
|
||||
return metrics
|
||||
|
||||
@self.app.get("/dag")
|
||||
async def get_full_dag():
|
||||
"""Get the complete DAG structure."""
|
||||
return asdict(self.runner.dag)
|
||||
|
||||
def start(self, host: str = "0.0.0.0", port: int = 8000):
|
||||
"""Start the FastAPI server."""
|
||||
uvicorn.run(self.app, host=host, port=port)
|
||||
|
||||
class TaskOrchestrator:
|
||||
"""Main orchestrator that runs tasks and API server."""
|
||||
|
||||
def __init__(self, tasks_dir: str, api_port: int = 8000):
|
||||
self.runner = EnhancedTaskRunner(tasks_dir)
|
||||
self.api = TaskRunnerAPI(self.runner)
|
||||
self.api_thread = None
|
||||
|
||||
def start_api_server(self, port: int = 8000):
|
||||
"""Start API server in a separate thread."""
|
||||
self.api_thread = threading.Thread(
|
||||
target=self.api.start,
|
||||
args=("0.0.0.0", port),
|
||||
daemon=True
|
||||
)
|
||||
self.api_thread.start()
|
||||
print(f"API server started on http://0.0.0.0:{port}")
|
||||
|
||||
def run(self):
|
||||
"""Run the task orchestration."""
|
||||
# Start API server
|
||||
self.start_api_server()
|
||||
|
||||
# Reset and run tasks
|
||||
self.runner.reset()
|
||||
try:
|
||||
self.runner.run()
|
||||
except Exception as e:
|
||||
print(f"Error during execution: {e}")
|
||||
self.runner.dag.state = "FAILED"
|
||||
self.runner.dag.end_time = datetime.now().isoformat()
|
||||
self.runner._save_dag()
|
||||
|
||||
print("\nExecution completed. API server still running.")
|
||||
print("Press Ctrl+C to stop the API server.")
|
||||
|
||||
try:
|
||||
# Keep the main thread alive for API access
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
@@ -48,12 +48,3 @@ else
|
||||
uv pip install herolib@git+https://git.ourworld.tf/herocode/herolib_python.git --force-reinstall --no-cache-dir
|
||||
fi
|
||||
echo -e "${GREEN}✅ Dependencies installed${NC}"
|
||||
|
||||
# Create necessary directories
|
||||
mkdir -p static/css static/js static/images
|
||||
mkdir -p templates
|
||||
mkdir -p md
|
||||
|
||||
echo -e "${GREEN}✅ Directory structure verified${NC}"
|
||||
|
||||
echo -e "${GREEN}🎉 Installation complete! You can now run start_server.sh${NC}"
|
@@ -7,7 +7,7 @@ authors = [
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["peewee"]
|
||||
dependencies = ["peewee", "psutil>=5.9.0", "fastapi>=0.100.0", "uvicorn>=0.23.0", "toml>=0.10.2", "libtmux>=0.25.0"]
|
||||
|
||||
|
||||
[build-system]
|
||||
@@ -18,4 +18,4 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["herolib*"]
|
||||
include = ["herolib*"]
|
||||
|
238
uv.lock
generated
238
uv.lock
generated
@@ -1,20 +1,254 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = "==3.12"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.116.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "herolib"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "libtmux" },
|
||||
{ name = "peewee" },
|
||||
{ name = "psutil" },
|
||||
{ name = "toml" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "peewee" }]
|
||||
requires-dist = [
|
||||
{ name = "fastapi", specifier = ">=0.100.0" },
|
||||
{ name = "libtmux", specifier = ">=0.25.0" },
|
||||
{ name = "peewee" },
|
||||
{ name = "psutil", specifier = ">=5.9.0" },
|
||||
{ name = "toml", specifier = ">=0.10.2" },
|
||||
{ name = "uvicorn", specifier = ">=0.23.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libtmux"
|
||||
version = "0.46.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/aa/7e1dcaa097156d6f3a7d8669be4389dced997feeb81744e3ff4681d65ee8/libtmux-0.46.2.tar.gz", hash = "sha256:9a398fec5d714129c8344555d466e1a903dfc0f741ba07aabe75a8ceb25c5dda", size = 346887, upload-time = "2025-05-26T19:40:04.096Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/2f/9d207039fcfa00d3b30e4d765f062fbcc42c873c7518a8cfebb3eafd00e0/libtmux-0.46.2-py3-none-any.whl", hash = "sha256:6c32dbf22bde8e5e33b2714a4295f6e838dc640f337cd4c085a044f6828c7793", size = 60873, upload-time = "2025-05-26T19:40:02.284Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peewee"
|
||||
version = "3.18.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/89/76f6f1b744c8608e0d416b588b9d63c2a500ff800065ae610f7c80f532d6/peewee-3.18.2.tar.gz", hash = "sha256:77a54263eb61aff2ea72f63d2eeb91b140c25c1884148e28e4c0f7c4f64996a0", size = 949220, upload-time = "2025-07-08T12:52:03.941Z" }
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.47.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.35.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
||||
]
|
||||
|
Reference in New Issue
Block a user