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