Files
timmy-local/tools/network_tools.py
Allegro f71e31585b Initial Uni-Wizard architecture
- Single harness for all API interactions
- Unified tool registry with routing
- System, Git, Network tool layers
- Local-first, self-healing design
2026-03-30 17:15:11 +00:00

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()