Source code for apifrom.decorators.web

"""
Web decorator for the APIFromAnything library.

This module provides the Web decorator, which enhances API endpoints with
automatic HTML rendering capabilities, making them more user-friendly
when accessed directly from a web browser.
"""
import json
import functools
import inspect
from typing import Any, Callable, Dict, List, Optional, Union

from apifrom.core.response import Response
from apifrom.core.app import API


[docs] class Web: """ Decorator that enhances API endpoints with HTML rendering capabilities. When an endpoint decorated with @Web is accessed with an Accept header that includes 'text/html' (e.g., from a web browser), the response will be rendered as HTML. Otherwise, the response will be returned as JSON (the default API behavior). This makes API endpoints more user-friendly when accessed directly from a web browser, while maintaining their programmatic API functionality. Attributes: title (str): The title to display in the HTML page description (str): A description of the endpoint to display in the HTML theme (str): The theme to use for styling ('default', 'dark', 'light') template (str): Optional path to a custom HTML template Example: ```python from apifrom.core.app import API from apifrom.decorators.web import Web app = API() @app.api('/users') @Web(title="Users API", description="Get a list of users") def get_users(request): return [ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Charlie"} ] ``` """ def __init__( self, title: Optional[str] = None, description: Optional[str] = None, theme: str = "default", template: Optional[str] = None ): """ Initialize the Web decorator. Args: title: The title to display in the HTML page description: A description of the endpoint to display in the HTML theme: The theme to use for styling ('default', 'dark', 'light') template: Optional path to a custom HTML template """ self.title = title or "API Endpoint" self.description = description or "" self.theme = theme self.template = template def __call__(self, func: Callable) -> Callable: """ Apply the decorator to the function. Args: func: The function to decorate Returns: The decorated function """ is_coroutine = inspect.iscoroutinefunction(func) @functools.wraps(func) async def async_wrapper(*args, **kwargs): # Call the original function result = await func(*args, **kwargs) # If the result is already a Response, return it if isinstance(result, Response): return result # Check if the first argument is a request request = args[0] if args else None # If the request has an Accept header that includes text/html, # render the result as HTML if request and hasattr(request, 'headers') and 'accept' in request.headers: accept = request.headers['accept'].lower() if 'text/html' in accept: html = self._render_html(result) return Response( content=html, content_type="text/html", status_code=200 ) # Otherwise, return the result as is return result @functools.wraps(func) def sync_wrapper(*args, **kwargs): # Call the original function result = func(*args, **kwargs) # If the result is already a Response, return it if isinstance(result, Response): return result # Check if the first argument is a request request = args[0] if args else None # If the request has an Accept header that includes text/html, # render the result as HTML if request and hasattr(request, 'headers') and 'accept' in request.headers: accept = request.headers['accept'].lower() if 'text/html' in accept: html = self._render_html(result) return Response( content=html, content_type="text/html", status_code=200 ) # Otherwise, return the result as is return result return async_wrapper if is_coroutine else sync_wrapper def _render_html(self, data: Any) -> str: """ Render the data as HTML. Args: data: The data to render Returns: The HTML representation of the data """ if self.template: return self._render_with_template(data) html = f""" <!DOCTYPE html> <html> <head> <title>{self.title}</title> <style> {self._get_theme_styles()} </style> </head> <body> <div class="container"> <h1>{self.title}</h1> <p>{self.description}</p> <div class="content"> """ if isinstance(data, dict): html += self._render_dict(data) elif isinstance(data, list): html += self._render_list(data) else: html += f"<pre>{self._format_value(data)}</pre>" html += """ </div> </div> </body> </html> """ return html def _render_dict(self, data: Dict) -> str: """ Render a dictionary as HTML. Args: data: The dictionary to render Returns: The HTML representation of the dictionary """ html = "<table class='data-table'>" html += "<tr><th>Key</th><th>Value</th></tr>" for key, value in data.items(): html += f"<tr><td>{key}</td><td>{self._format_value(value)}</td></tr>" html += "</table>" return html def _render_list(self, data: List) -> str: """ Render a list as HTML. Args: data: The list to render Returns: The HTML representation of the list """ # If the list is empty, render an empty list if not data: return "<p>Empty list</p>" # If the list contains dictionaries, render a table if all(isinstance(item, dict) for item in data): # Get all unique keys from all dictionaries keys = set() for item in data: keys.update(item.keys()) keys = sorted(keys) html = "<table class='data-table'>" html += "<tr>" for key in keys: html += f"<th>{key}</th>" html += "</tr>" for item in data: html += "<tr>" for key in keys: value = item.get(key, "") html += f"<td>{self._format_value(value)}</td>" html += "</tr>" html += "</table>" return html # Otherwise, render a simple list html = "<ul>" for item in data: html += f"<li>{self._format_value(item)}</li>" html += "</ul>" return html def _format_value(self, value: Any) -> str: """ Format a value for HTML display. Args: value: The value to format Returns: The formatted value """ if isinstance(value, dict): return self._render_dict(value) elif isinstance(value, list): return self._render_list(value) elif value is None: return "<em>null</em>" elif isinstance(value, bool): return str(value).lower() elif isinstance(value, (int, float)): return str(value) else: return str(value) def _render_with_template(self, data: Any) -> str: """ Render the data using a custom template. Args: data: The data to render Returns: The HTML representation of the data using the template """ try: import jinja2 # Load the template env = jinja2.Environment( loader=jinja2.FileSystemLoader("./"), autoescape=jinja2.select_autoescape(['html', 'xml']) ) template = env.get_template(self.template) # Render the template with the data return template.render( title=self.title, description=self.description, data=data ) except ImportError: # If jinja2 is not installed, fall back to the default rendering return self._render_html(data) except jinja2.exceptions.TemplateNotFound: # If the template is not found, fall back to the default rendering return self._render_html(data) def _get_theme_styles(self) -> str: """ Get the CSS styles for the current theme. Returns: The CSS styles for the theme """ # Base styles for all themes base_styles = """ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; margin: 0; padding: 0; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } h1 { margin-top: 0; } .content { margin-top: 20px; } .data-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; } .data-table th, .data-table td { padding: 8px 12px; text-align: left; border: 1px solid; } .data-table th { font-weight: bold; } ul { margin: 0; padding-left: 20px; } """ # Theme-specific styles if self.theme == "dark": theme_styles = """ body { background-color: #1e1e1e; color: #f0f0f0; } .data-table th, .data-table td { border-color: #444; } .data-table th { background-color: #333; } .data-table tr:nth-child(even) { background-color: #2a2a2a; } a { color: #4da6ff; } """ elif self.theme == "light": theme_styles = """ body { background-color: #ffffff; color: #333333; } .data-table th, .data-table td { border-color: #ddd; } .data-table th { background-color: #f5f5f5; } .data-table tr:nth-child(even) { background-color: #f9f9f9; } a { color: #0066cc; } """ else: # default theme theme_styles = """ body { background-color: #f8f9fa; color: #212529; } .data-table th, .data-table td { border-color: #dee2e6; } .data-table th { background-color: #e9ecef; } .data-table tr:nth-child(even) { background-color: #f2f2f2; } a { color: #007bff; } """ return base_styles + theme_styles
app = API()