- Single harness for all API interactions - Unified tool registry with routing - System, Git, Network tool layers - Local-first, self-healing design
460 lines
12 KiB
Python
460 lines
12 KiB
Python
"""
|
|
Network Tools for Uni-Wizard
|
|
HTTP client and Gitea API integration
|
|
"""
|
|
|
|
import json
|
|
import urllib.request
|
|
import urllib.error
|
|
from typing import Dict, Optional, Any
|
|
from base64 import b64encode
|
|
|
|
from .registry import registry
|
|
|
|
|
|
class HTTPClient:
|
|
"""Simple HTTP client for API calls"""
|
|
|
|
def __init__(self, base_url: str = None, auth: tuple = None):
|
|
self.base_url = base_url
|
|
self.auth = auth
|
|
|
|
def _make_request(
|
|
self,
|
|
method: str,
|
|
url: str,
|
|
data: Dict = None,
|
|
headers: Dict = None
|
|
) -> tuple:
|
|
"""Make HTTP request and return (body, status_code, error)"""
|
|
try:
|
|
# Build full URL
|
|
full_url = url
|
|
if self.base_url and not url.startswith('http'):
|
|
full_url = f"{self.base_url.rstrip('/')}/{url.lstrip('/')}"
|
|
|
|
# Prepare data
|
|
body = None
|
|
if data:
|
|
body = json.dumps(data).encode('utf-8')
|
|
|
|
# Build request
|
|
req = urllib.request.Request(
|
|
full_url,
|
|
data=body,
|
|
method=method
|
|
)
|
|
|
|
# Add headers
|
|
req.add_header('Content-Type', 'application/json')
|
|
if headers:
|
|
for key, value in headers.items():
|
|
req.add_header(key, value)
|
|
|
|
# Add auth
|
|
if self.auth:
|
|
username, password = self.auth
|
|
credentials = b64encode(f"{username}:{password}".encode()).decode()
|
|
req.add_header('Authorization', f'Basic {credentials}')
|
|
|
|
# Make request
|
|
with urllib.request.urlopen(req, timeout=30) as response:
|
|
return response.read().decode('utf-8'), response.status, None
|
|
|
|
except urllib.error.HTTPError as e:
|
|
return e.read().decode('utf-8'), e.code, str(e)
|
|
except Exception as e:
|
|
return None, 0, str(e)
|
|
|
|
def get(self, url: str) -> tuple:
|
|
return self._make_request('GET', url)
|
|
|
|
def post(self, url: str, data: Dict) -> tuple:
|
|
return self._make_request('POST', url, data)
|
|
|
|
def put(self, url: str, data: Dict) -> tuple:
|
|
return self._make_request('PUT', url, data)
|
|
|
|
def delete(self, url: str) -> tuple:
|
|
return self._make_request('DELETE', url)
|
|
|
|
|
|
def http_get(url: str) -> str:
|
|
"""
|
|
Perform HTTP GET request.
|
|
|
|
Args:
|
|
url: URL to fetch
|
|
|
|
Returns:
|
|
Response body or error message
|
|
"""
|
|
client = HTTPClient()
|
|
body, status, error = client.get(url)
|
|
|
|
if error:
|
|
return f"Error (HTTP {status}): {error}"
|
|
|
|
return body
|
|
|
|
|
|
def http_post(url: str, body: Dict) -> str:
|
|
"""
|
|
Perform HTTP POST request with JSON body.
|
|
|
|
Args:
|
|
url: URL to post to
|
|
body: JSON body as dictionary
|
|
|
|
Returns:
|
|
Response body or error message
|
|
"""
|
|
client = HTTPClient()
|
|
response_body, status, error = client.post(url, body)
|
|
|
|
if error:
|
|
return f"Error (HTTP {status}): {error}"
|
|
|
|
return response_body
|
|
|
|
|
|
# Gitea API Tools
|
|
GITEA_URL = "http://143.198.27.163:3000"
|
|
GITEA_USER = "timmy"
|
|
GITEA_PASS = "" # Should be configured
|
|
|
|
|
|
def gitea_create_issue(
|
|
repo: str = "Timmy_Foundation/timmy-home",
|
|
title: str = None,
|
|
body: str = None,
|
|
labels: list = None
|
|
) -> str:
|
|
"""
|
|
Create a Gitea issue.
|
|
|
|
Args:
|
|
repo: Repository path (owner/repo)
|
|
title: Issue title (required)
|
|
body: Issue body
|
|
labels: List of label names
|
|
|
|
Returns:
|
|
Created issue URL or error
|
|
"""
|
|
if not title:
|
|
return "Error: title is required"
|
|
|
|
try:
|
|
client = HTTPClient(
|
|
base_url=GITEA_URL,
|
|
auth=(GITEA_USER, GITEA_PASS) if GITEA_PASS else None
|
|
)
|
|
|
|
data = {
|
|
"title": title,
|
|
"body": body or ""
|
|
}
|
|
if labels:
|
|
data["labels"] = labels
|
|
|
|
response, status, error = client.post(
|
|
f"/api/v1/repos/{repo}/issues",
|
|
data
|
|
)
|
|
|
|
if error:
|
|
return f"Error creating issue: {error}"
|
|
|
|
result = json.loads(response)
|
|
return f"✓ Issue created: #{result['number']} - {result['html_url']}"
|
|
|
|
except Exception as e:
|
|
return f"Error: {str(e)}"
|
|
|
|
|
|
def gitea_comment(
|
|
repo: str = "Timmy_Foundation/timmy-home",
|
|
issue_number: int = None,
|
|
body: str = None
|
|
) -> str:
|
|
"""
|
|
Comment on a Gitea issue.
|
|
|
|
Args:
|
|
repo: Repository path
|
|
issue_number: Issue number (required)
|
|
body: Comment body (required)
|
|
|
|
Returns:
|
|
Comment result
|
|
"""
|
|
if not issue_number or not body:
|
|
return "Error: issue_number and body are required"
|
|
|
|
try:
|
|
client = HTTPClient(
|
|
base_url=GITEA_URL,
|
|
auth=(GITEA_USER, GITEA_PASS) if GITEA_PASS else None
|
|
)
|
|
|
|
response, status, error = client.post(
|
|
f"/api/v1/repos/{repo}/issues/{issue_number}/comments",
|
|
{"body": body}
|
|
)
|
|
|
|
if error:
|
|
return f"Error posting comment: {error}"
|
|
|
|
result = json.loads(response)
|
|
return f"✓ Comment posted: {result['html_url']}"
|
|
|
|
except Exception as e:
|
|
return f"Error: {str(e)}"
|
|
|
|
|
|
def gitea_list_issues(
|
|
repo: str = "Timmy_Foundation/timmy-home",
|
|
state: str = "open",
|
|
assignee: str = None
|
|
) -> str:
|
|
"""
|
|
List Gitea issues.
|
|
|
|
Args:
|
|
repo: Repository path
|
|
state: open, closed, or all
|
|
assignee: Filter by assignee username
|
|
|
|
Returns:
|
|
JSON list of issues
|
|
"""
|
|
try:
|
|
client = HTTPClient(
|
|
base_url=GITEA_URL,
|
|
auth=(GITEA_USER, GITEA_PASS) if GITEA_PASS else None
|
|
)
|
|
|
|
url = f"/api/v1/repos/{repo}/issues?state={state}"
|
|
if assignee:
|
|
url += f"&assignee={assignee}"
|
|
|
|
response, status, error = client.get(url)
|
|
|
|
if error:
|
|
return f"Error fetching issues: {error}"
|
|
|
|
issues = json.loads(response)
|
|
|
|
# Simplify output
|
|
simplified = []
|
|
for issue in issues:
|
|
simplified.append({
|
|
"number": issue["number"],
|
|
"title": issue["title"],
|
|
"state": issue["state"],
|
|
"assignee": issue.get("assignee", {}).get("login") if issue.get("assignee") else None,
|
|
"url": issue["html_url"]
|
|
})
|
|
|
|
return json.dumps({
|
|
"count": len(simplified),
|
|
"issues": simplified
|
|
}, indent=2)
|
|
|
|
except Exception as e:
|
|
return f"Error: {str(e)}"
|
|
|
|
|
|
def gitea_get_issue(repo: str = "Timmy_Foundation/timmy-home", issue_number: int = None) -> str:
|
|
"""
|
|
Get details of a specific Gitea issue.
|
|
|
|
Args:
|
|
repo: Repository path
|
|
issue_number: Issue number (required)
|
|
|
|
Returns:
|
|
Issue details
|
|
"""
|
|
if not issue_number:
|
|
return "Error: issue_number is required"
|
|
|
|
try:
|
|
client = HTTPClient(
|
|
base_url=GITEA_URL,
|
|
auth=(GITEA_USER, GITEA_PASS) if GITEA_PASS else None
|
|
)
|
|
|
|
response, status, error = client.get(
|
|
f"/api/v1/repos/{repo}/issues/{issue_number}"
|
|
)
|
|
|
|
if error:
|
|
return f"Error fetching issue: {error}"
|
|
|
|
issue = json.loads(response)
|
|
|
|
return json.dumps({
|
|
"number": issue["number"],
|
|
"title": issue["title"],
|
|
"body": issue["body"][:500] + "..." if len(issue["body"]) > 500 else issue["body"],
|
|
"state": issue["state"],
|
|
"assignee": issue.get("assignee", {}).get("login") if issue.get("assignee") else None,
|
|
"created_at": issue["created_at"],
|
|
"url": issue["html_url"]
|
|
}, indent=2)
|
|
|
|
except Exception as e:
|
|
return f"Error: {str(e)}"
|
|
|
|
|
|
# Register all network tools
|
|
def register_all():
|
|
registry.register(
|
|
name="http_get",
|
|
handler=http_get,
|
|
description="Perform HTTP GET request",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"url": {
|
|
"type": "string",
|
|
"description": "URL to fetch"
|
|
}
|
|
},
|
|
"required": ["url"]
|
|
},
|
|
category="network"
|
|
)
|
|
|
|
registry.register(
|
|
name="http_post",
|
|
handler=http_post,
|
|
description="Perform HTTP POST request with JSON body",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"url": {
|
|
"type": "string",
|
|
"description": "URL to post to"
|
|
},
|
|
"body": {
|
|
"type": "object",
|
|
"description": "JSON body as dictionary"
|
|
}
|
|
},
|
|
"required": ["url", "body"]
|
|
},
|
|
category="network"
|
|
)
|
|
|
|
registry.register(
|
|
name="gitea_create_issue",
|
|
handler=gitea_create_issue,
|
|
description="Create a Gitea issue",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"repo": {
|
|
"type": "string",
|
|
"description": "Repository path (owner/repo)",
|
|
"default": "Timmy_Foundation/timmy-home"
|
|
},
|
|
"title": {
|
|
"type": "string",
|
|
"description": "Issue title"
|
|
},
|
|
"body": {
|
|
"type": "string",
|
|
"description": "Issue body"
|
|
},
|
|
"labels": {
|
|
"type": "array",
|
|
"description": "List of label names",
|
|
"items": {"type": "string"}
|
|
}
|
|
},
|
|
"required": ["title"]
|
|
},
|
|
category="network"
|
|
)
|
|
|
|
registry.register(
|
|
name="gitea_comment",
|
|
handler=gitea_comment,
|
|
description="Comment on a Gitea issue",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"repo": {
|
|
"type": "string",
|
|
"description": "Repository path",
|
|
"default": "Timmy_Foundation/timmy-home"
|
|
},
|
|
"issue_number": {
|
|
"type": "integer",
|
|
"description": "Issue number"
|
|
},
|
|
"body": {
|
|
"type": "string",
|
|
"description": "Comment body"
|
|
}
|
|
},
|
|
"required": ["issue_number", "body"]
|
|
},
|
|
category="network"
|
|
)
|
|
|
|
registry.register(
|
|
name="gitea_list_issues",
|
|
handler=gitea_list_issues,
|
|
description="List Gitea issues",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"repo": {
|
|
"type": "string",
|
|
"description": "Repository path",
|
|
"default": "Timmy_Foundation/timmy-home"
|
|
},
|
|
"state": {
|
|
"type": "string",
|
|
"enum": ["open", "closed", "all"],
|
|
"description": "Issue state",
|
|
"default": "open"
|
|
},
|
|
"assignee": {
|
|
"type": "string",
|
|
"description": "Filter by assignee username"
|
|
}
|
|
}
|
|
},
|
|
category="network"
|
|
)
|
|
|
|
registry.register(
|
|
name="gitea_get_issue",
|
|
handler=gitea_get_issue,
|
|
description="Get details of a specific Gitea issue",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"repo": {
|
|
"type": "string",
|
|
"description": "Repository path",
|
|
"default": "Timmy_Foundation/timmy-home"
|
|
},
|
|
"issue_number": {
|
|
"type": "integer",
|
|
"description": "Issue number"
|
|
}
|
|
},
|
|
"required": ["issue_number"]
|
|
},
|
|
category="network"
|
|
)
|
|
|
|
|
|
register_all()
|