Skip to main content
Glama
client.py13.7 kB
""" SiteBay API client for handling all API communications """ import httpx from typing import Any, Dict, List, Optional, Union from .auth import SiteBayAuth from .exceptions import APIError, AuthenticationError, SiteNotFoundError, ValidationError class SiteBayClient: """Client for interacting with SiteBay API""" BASE_URL = "https://my.sitebay.org" API_PREFIX = "/f/api/v1" def __init__(self, auth: SiteBayAuth, timeout: float = 30.0): """ Initialize SiteBay API client Args: auth: Authentication instance timeout: Request timeout in seconds """ self.auth = auth self.timeout = timeout self._client = httpx.AsyncClient( base_url=self.BASE_URL, timeout=timeout, headers=self.auth.get_headers(), follow_redirects=True ) async def __aenter__(self): """Async context manager entry""" return self async def __aexit__(self, exc_type, exc_val, exc_tb): """Async context manager exit""" await self._client.aclose() async def close(self): """Close the HTTP client""" await self._client.aclose() def _get_url(self, endpoint: str) -> str: """Get full URL for an API endpoint""" if not endpoint.startswith("/"): endpoint = "/" + endpoint return f"{self.API_PREFIX}{endpoint}" def _format_validation_error(self, error_data: dict) -> str: """ Format validation error details for better readability Args: error_data: Error response from API Returns: Formatted error message with field-specific details """ if not error_data: return "Validation failed" # Handle FastAPI/Pydantic validation error format if "detail" in error_data: detail = error_data["detail"] # If detail is a string, return it directly if isinstance(detail, str): return f"Validation Error: {detail}" # If detail is a list of validation errors if isinstance(detail, list): error_messages = [] for error in detail: if isinstance(error, dict): loc = error.get("loc", []) msg = error.get("msg", "Invalid value") field = " -> ".join(str(x) for x in loc) if loc else "unknown field" error_messages.append(f"{field}: {msg}") if error_messages: return f"Validation Error:\n" + "\n".join(f" • {msg}" for msg in error_messages) # Handle other error formats if "message" in error_data: return f"Validation Error: {error_data['message']}" # Fallback - try to extract any useful information if "errors" in error_data: errors = error_data["errors"] if isinstance(errors, dict): error_messages = [] for field, messages in errors.items(): if isinstance(messages, list): for msg in messages: error_messages.append(f"{field}: {msg}") else: error_messages.append(f"{field}: {messages}") if error_messages: return f"Validation Error:\n" + "\n".join(f" • {msg}" for msg in error_messages) return f"Validation Error: {str(error_data)}" def _extract_field_errors(self, error_data: dict) -> dict[str, str]: """ Extract field-specific errors for programmatic access Args: error_data: Error response from API Returns: Dictionary mapping field names to error messages """ field_errors: dict[str, str] = {} if not error_data: return field_errors # Handle FastAPI/Pydantic validation error format if "detail" in error_data and isinstance(error_data["detail"], list): for error in error_data["detail"]: if isinstance(error, dict): loc = error.get("loc", []) msg = error.get("msg", "Invalid value") field = " -> ".join(str(x) for x in loc) if loc else "unknown" field_errors[field] = msg # Handle other error formats elif "errors" in error_data and isinstance(error_data["errors"], dict): for field, messages in error_data["errors"].items(): if isinstance(messages, list): field_errors[field] = "; ".join(messages) else: field_errors[field] = str(messages) return field_errors async def _request( self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None, **kwargs ) -> Any: """ Make an HTTP request to the SiteBay API Args: method: HTTP method (GET, POST, PATCH, DELETE) endpoint: API endpoint path params: Query parameters json_data: JSON body data **kwargs: Additional httpx request arguments Returns: Response data (parsed JSON or raw response) Raises: APIError: For API errors AuthenticationError: For authentication failures """ url = self._get_url(endpoint) try: response = await self._client.request( method=method, url=url, params=params, json=json_data, **kwargs ) # Handle different response codes if response.status_code == 401: raise AuthenticationError("Invalid or expired API token") elif response.status_code == 404: raise SiteNotFoundError("Requested resource not found") elif response.status_code == 422: # Handle validation errors with detailed information try: error_data = response.json() error_message = self._format_validation_error(error_data) field_errors = self._extract_field_errors(error_data) except Exception: error_message = f"Validation Error: {response.text}" error_data = None field_errors = {} raise ValidationError( message=error_message, field_errors=field_errors ) elif response.status_code >= 400: try: error_data = response.json() error_message = error_data.get("detail", f"API Error: {response.status_code}") except Exception: error_message = f"API Error: {response.status_code} - {response.text}" raise APIError( message=error_message, status_code=response.status_code, response_data=error_data if 'error_data' in locals() else None ) # Try to parse JSON response try: return response.json() except Exception: # Return raw response if not JSON return response.text except httpx.RequestError as e: raise APIError(f"Network error: {str(e)}") async def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Any: """Make GET request""" return await self._request("GET", endpoint, params=params) async def post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Any: """Make POST request""" return await self._request("POST", endpoint, params=params, json_data=json_data) async def patch(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Any: """Make PATCH request""" return await self._request("PATCH", endpoint, params=params, json_data=json_data) async def delete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Any: """Make DELETE request""" return await self._request("DELETE", endpoint, params=params) # Site Management Methods async def list_sites(self, team_id: Optional[str] = None) -> Union[List[Dict[str, Any]], str]: """List all sites for the user""" params = {"team_id": team_id} if team_id else None response = await self.get("/site", params=params) # Handle case where API returns error as string if isinstance(response, str): return response # Handle normal dict response if isinstance(response, dict): return response.get("results", []) # Handle unexpected response format return f"Unexpected response format: {type(response).__name__}" async def get_site(self, fqdn: str) -> Dict[str, Any]: """Get details for a specific site""" return await self.get(f"/site/{fqdn}") async def create_site(self, site_data: Dict[str, Any]) -> Dict[str, Any]: """Create a new site""" return await self.post("/site", json_data=site_data) async def update_site(self, fqdn: str, site_data: Dict[str, Any]) -> Dict[str, Any]: """Update an existing site""" return await self.patch(f"/site/{fqdn}", json_data=site_data) async def delete_site(self, fqdn: str) -> Dict[str, Any]: """Delete a site""" return await self.delete(f"/site/{fqdn}") # Site Operations Methods async def execute_shell_command( self, fqdn: str, cmd: str, cwd: Optional[str] = None, auto_track_dir: Optional[bool] = None, ) -> Any: """Execute a shell command on a site""" payload: Dict[str, Any] = {"cmd": cmd} if cwd is not None: payload["cwd"] = cwd if auto_track_dir is not None: payload["auto_track_dir"] = auto_track_dir return await self.post(f"/site/{fqdn}/cmd", json_data=payload) async def edit_file( self, fqdn: str, file_path: str, file_edit_using_search_replace_blocks: str, ) -> str: """Edit a file in the site's wp-content directory""" return await self.post( f"/site/{fqdn}/wpfile_diff_edit", json_data={ "file_path": file_path, "file_edit_using_search_replace_blocks": file_edit_using_search_replace_blocks, }, ) # Site events endpoint removed (not present in schema) # Staging methods removed (no longer supported) # Backup/Restore Methods async def get_backup_commits(self, fqdn: str, number_to_fetch: int = 1) -> List[Dict[str, Any]]: """Get backup commits for a site""" params = {"number_to_fetch": number_to_fetch} return await self.get(f"/site/{fqdn}/pit_restore/commits", params=params) async def create_restore(self, fqdn: str, restore_data: Dict[str, Any]) -> Dict[str, Any]: """Create a point-in-time restore""" return await self.post(f"/site/{fqdn}/pit_restore", json_data=restore_data) async def list_restores(self, fqdn: str) -> List[Dict[str, Any]]: """List all restores for a site""" response = await self.get(f"/site/{fqdn}/pit_restore") return response.get("results", []) # External Path Methods # External path methods removed (no longer supported) # Proxy Methods async def wordpress_proxy(self, proxy_data: Dict[str, Any]) -> Any: """Proxy request to WordPress API""" return await self.post("/wp-proxy", json_data=proxy_data) async def shopify_proxy(self, proxy_data: Dict[str, Any]) -> Any: """Proxy request to Shopify API""" return await self.post("/shopify-proxy", json_data=proxy_data) async def posthog_proxy(self, proxy_data: Dict[str, Any]) -> Any: """Proxy request to PostHog API""" return await self.post("/posthog-proxy", json_data=proxy_data) # Team Methods async def list_teams(self) -> List[Dict[str, Any]]: """List user teams""" response = await self.get("/team") return response.get("results", []) # Ready-made site catalog async def list_ready_made_sites(self) -> List[Dict[str, Any]]: """List available ready-made sites""" response = await self.get("/ready_made_site") return response.get("results", []) # Account Methods async def get_affiliate_referrals(self) -> List[Dict[str, Any]]: """Get affiliate referrals""" response = await self.get("/account/referred_user") return response.get("results", []) async def create_checkout_session(self, checkout_data: Dict[str, Any]) -> Dict[str, Any]: """Create Stripe checkout session""" return await self.post("/create_checkout_session", json_data=checkout_data)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sitebay/sitebay-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server