Tidy endpoints
All checks were successful
CI / Build and push Docker image (push) Successful in 1m27s
All checks were successful
CI / Build and push Docker image (push) Successful in 1m27s
This commit is contained in:
parent
c1f7cfbe02
commit
610d52a8cb
7 changed files with 163 additions and 175 deletions
|
@ -2,6 +2,8 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from . import __description__, __title__, __version__
|
||||
|
@ -9,6 +11,23 @@ from .core.middleware import setup_middleware
|
|||
from .core.registry import registry
|
||||
from .models.base import HealthResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi.routing import APIRoute
|
||||
|
||||
|
||||
def generate_unique_id(route: APIRoute) -> str:
|
||||
"""Generate a unique ID for a route.
|
||||
|
||||
Args:
|
||||
route (APIRoute): The route to generate an ID for.
|
||||
|
||||
Returns:
|
||||
str: The unique ID for the route.
|
||||
"""
|
||||
if route.tags:
|
||||
return f"{route.tags[0]}_{route.name}"
|
||||
return route.name
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Create and configure the FastAPI application.
|
||||
|
@ -22,6 +41,7 @@ def create_app() -> FastAPI:
|
|||
description=__description__,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
generate_unique_id_function=generate_unique_id,
|
||||
)
|
||||
|
||||
# Setup middleware
|
||||
|
|
|
@ -33,7 +33,7 @@ class MemoryTool(BaseTool):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_entities(req: CreateEntitiesRequest) -> list[Entity]:
|
||||
def create_entities(req: CreateEntitiesRequest) -> list[Entity]:
|
||||
"""Create multiple entities in the graph.
|
||||
|
||||
Returns:
|
||||
|
@ -47,7 +47,7 @@ class MemoryTool(BaseTool):
|
|||
return new_entities
|
||||
|
||||
@staticmethod
|
||||
def _create_relations(req: CreateRelationsRequest) -> list[Relation]:
|
||||
def create_relations(req: CreateRelationsRequest) -> list[Relation]:
|
||||
"""Create multiple relations between entities.
|
||||
|
||||
Returns:
|
||||
|
@ -61,7 +61,7 @@ class MemoryTool(BaseTool):
|
|||
return new
|
||||
|
||||
@staticmethod
|
||||
def _add_observations(req: AddObservationsRequest) -> list[dict[str, str | list[str]]]:
|
||||
def add_observations(req: AddObservationsRequest) -> list[dict[str, str | list[str]]]:
|
||||
"""Add new observations to existing entities.
|
||||
|
||||
Returns:
|
||||
|
@ -87,7 +87,7 @@ class MemoryTool(BaseTool):
|
|||
return results
|
||||
|
||||
@staticmethod
|
||||
def _delete_entities(req: DeleteEntitiesRequest) -> dict[str, str]:
|
||||
def delete_entities(req: DeleteEntitiesRequest) -> dict[str, str]:
|
||||
"""Delete entities and associated relations.
|
||||
|
||||
Returns:
|
||||
|
@ -104,7 +104,7 @@ class MemoryTool(BaseTool):
|
|||
return {"message": "Entities deleted successfully"}
|
||||
|
||||
@staticmethod
|
||||
def _delete_observations(req: DeleteObservationsRequest) -> dict[str, str]:
|
||||
def delete_observations(req: DeleteObservationsRequest) -> dict[str, str]:
|
||||
"""Delete specific observations from entities.
|
||||
|
||||
Returns:
|
||||
|
@ -123,7 +123,7 @@ class MemoryTool(BaseTool):
|
|||
return {"message": "Observations deleted successfully"}
|
||||
|
||||
@staticmethod
|
||||
def _delete_relations(req: DeleteRelationsRequest) -> dict[str, str]:
|
||||
def delete_relations(req: DeleteRelationsRequest) -> dict[str, str]:
|
||||
"""Delete relations from the graph.
|
||||
|
||||
Returns:
|
||||
|
@ -138,7 +138,7 @@ class MemoryTool(BaseTool):
|
|||
return {"message": "Relations deleted successfully"}
|
||||
|
||||
@staticmethod
|
||||
def _read_graph() -> KnowledgeGraph:
|
||||
def read_graph() -> KnowledgeGraph:
|
||||
"""Read entire knowledge graph.
|
||||
|
||||
Returns:
|
||||
|
@ -147,7 +147,7 @@ class MemoryTool(BaseTool):
|
|||
return read_graph_file()
|
||||
|
||||
@staticmethod
|
||||
def _search_nodes(req: SearchNodesRequest) -> KnowledgeGraph:
|
||||
def search_nodes(req: SearchNodesRequest) -> KnowledgeGraph:
|
||||
"""Search for nodes by keyword.
|
||||
|
||||
Returns:
|
||||
|
@ -167,7 +167,7 @@ class MemoryTool(BaseTool):
|
|||
return KnowledgeGraph(entities=entities, relations=relations)
|
||||
|
||||
@staticmethod
|
||||
def _open_nodes(req: OpenNodesRequest) -> KnowledgeGraph:
|
||||
def open_nodes(req: OpenNodesRequest) -> KnowledgeGraph:
|
||||
"""Open specific nodes by name.
|
||||
|
||||
Returns:
|
||||
|
@ -185,60 +185,60 @@ class MemoryTool(BaseTool):
|
|||
|
||||
router.add_api_route(
|
||||
"/create_entities",
|
||||
MemoryTool._create_entities,
|
||||
MemoryTool.create_entities,
|
||||
methods=["POST"],
|
||||
summary="Create multiple entities in the graph",
|
||||
summary="Store new entities (people, places, concepts) with their properties and observations",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/create_relations",
|
||||
MemoryTool._create_relations,
|
||||
MemoryTool.create_relations,
|
||||
methods=["POST"],
|
||||
summary="Create multiple relations between entities",
|
||||
summary="Connect entities with relationships (works_at, lives_in, knows, etc.)",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/add_observations",
|
||||
MemoryTool._add_observations,
|
||||
MemoryTool.add_observations,
|
||||
methods=["POST"],
|
||||
summary="Add new observations to existing entities",
|
||||
summary="Add new facts or observations to entities you've already stored",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/delete_entities",
|
||||
MemoryTool._delete_entities,
|
||||
MemoryTool.delete_entities,
|
||||
methods=["POST"],
|
||||
summary="Delete entities and associated relations",
|
||||
summary="Remove entities and all their connections from memory",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/delete_observations",
|
||||
MemoryTool._delete_observations,
|
||||
MemoryTool.delete_observations,
|
||||
methods=["POST"],
|
||||
summary="Delete specific observations from entities",
|
||||
summary="Remove specific facts or observations from entities",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/delete_relations",
|
||||
MemoryTool._delete_relations,
|
||||
MemoryTool.delete_relations,
|
||||
methods=["POST"],
|
||||
summary="Delete relations from the graph",
|
||||
summary="Remove specific relationships between entities",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/read_graph",
|
||||
MemoryTool._read_graph,
|
||||
MemoryTool.read_graph,
|
||||
methods=["GET"],
|
||||
response_model=KnowledgeGraph,
|
||||
summary="Read entire knowledge graph",
|
||||
summary="Retrieve all stored entities and relationships from memory",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/search_nodes",
|
||||
MemoryTool._search_nodes,
|
||||
MemoryTool.search_nodes,
|
||||
methods=["POST"],
|
||||
response_model=KnowledgeGraph,
|
||||
summary="Search for nodes by keyword",
|
||||
summary="Find entities by searching names, types, or observations",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/open_nodes",
|
||||
MemoryTool._open_nodes,
|
||||
MemoryTool.open_nodes,
|
||||
methods=["POST"],
|
||||
response_model=KnowledgeGraph,
|
||||
summary="Open specific nodes by name",
|
||||
summary="Retrieve specific entities and their connections by exact name",
|
||||
)
|
||||
|
||||
return router
|
||||
|
|
|
@ -4,10 +4,9 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import os
|
||||
from typing import Annotated
|
||||
|
||||
import requests
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from openapi_mcp_server.tools.base import BaseTool
|
||||
|
||||
|
@ -36,34 +35,13 @@ class SearxngTool(BaseTool):
|
|||
"""Return the FastAPI router for searxng tool endpoints."""
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/search", response_model=SearchResponse)
|
||||
def search_get(
|
||||
q: Annotated[str | None, Query(description="Search query")] = None,
|
||||
categories: Annotated[
|
||||
str | None, Query(description="Comma-separated categories")
|
||||
] = None,
|
||||
engines: Annotated[str | None, Query(description="Comma-separated engines")] = None,
|
||||
language: Annotated[str, Query(description="Language code")] = "en",
|
||||
response_format: Annotated[str, Query(description="Response format")] = "json",
|
||||
pageno: Annotated[int, Query(description="Page number")] = 1,
|
||||
) -> SearchResponse:
|
||||
"""Perform search via GET request.
|
||||
|
||||
Returns:
|
||||
SearchResponse: Search results from SearXNG.
|
||||
|
||||
Raises:
|
||||
HTTPException: If query parameter 'q' is not provided.
|
||||
"""
|
||||
if not q:
|
||||
raise HTTPException(status_code=422, detail="Query parameter 'q' is required.")
|
||||
return SearxngTool._perform_search(
|
||||
q, categories, engines, language, response_format, pageno
|
||||
)
|
||||
|
||||
@router.post("/search", response_model=SearchResponse)
|
||||
def search_post(request: SearchRequest) -> SearchResponse:
|
||||
"""Perform search via POST request.
|
||||
@router.post(
|
||||
"/search",
|
||||
response_model=SearchResponse,
|
||||
summary="Search the web across multiple search engines",
|
||||
)
|
||||
def search(request: SearchRequest) -> SearchResponse:
|
||||
"""Search the web across multiple search engines.
|
||||
|
||||
Returns:
|
||||
SearchResponse: Search results from SearXNG.
|
||||
|
@ -77,8 +55,12 @@ class SearxngTool(BaseTool):
|
|||
request.pageno,
|
||||
)
|
||||
|
||||
@router.get("/categories", response_model=CategoriesResponse)
|
||||
def get_categories() -> CategoriesResponse:
|
||||
@router.get(
|
||||
"/categories",
|
||||
response_model=CategoriesResponse,
|
||||
summary="Get available search categories (general, images, news, etc)",
|
||||
)
|
||||
def categories() -> CategoriesResponse:
|
||||
"""Get available search categories from SearXNG.
|
||||
|
||||
Returns:
|
||||
|
@ -109,8 +91,12 @@ class SearxngTool(BaseTool):
|
|||
note=f"Using default categories (SearXNG config unavailable: {e!s})",
|
||||
)
|
||||
|
||||
@router.get("/engines", response_model=EnginesResponse)
|
||||
def get_engines() -> EnginesResponse:
|
||||
@router.get(
|
||||
"/engines",
|
||||
response_model=EnginesResponse,
|
||||
summary="Get list of available search engines (Google, Bing, DuckDuckGo, etc)",
|
||||
)
|
||||
def engines() -> EnginesResponse:
|
||||
"""Get available search engines from SearXNG.
|
||||
|
||||
Returns:
|
||||
|
|
|
@ -7,13 +7,6 @@ from typing import Literal
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class FormatTimeInput(BaseModel):
|
||||
"""Input model for formatting time."""
|
||||
|
||||
format: str = Field("%Y-%m-%d %H:%M:%S", description="Python strftime format string")
|
||||
timezone: str = Field("UTC", description="IANA timezone name (e.g., UTC, America/New_York)")
|
||||
|
||||
|
||||
class ConvertTimeInput(BaseModel):
|
||||
"""Input model for converting time between timezones."""
|
||||
|
||||
|
@ -45,24 +38,19 @@ class ParseTimestampInput(BaseModel):
|
|||
timezone: str = Field("UTC", description="Assumed timezone if none is specified in input")
|
||||
|
||||
|
||||
class UnixToIsoInput(BaseModel):
|
||||
"""Input model for converting Unix timestamp to ISO format."""
|
||||
|
||||
timestamp: float = Field(..., description="Unix epoch timestamp (seconds since 1970-01-01)")
|
||||
timezone: str = Field("UTC", description="Target timezone for output (defaults to UTC)")
|
||||
|
||||
|
||||
class TimeResponse(BaseModel):
|
||||
"""Response model for UTC time."""
|
||||
|
||||
utc: str = Field(..., description="UTC time in ISO format")
|
||||
|
||||
|
||||
class LocalTimeResponse(BaseModel):
|
||||
"""Response model for local time."""
|
||||
|
||||
local_time: str = Field(..., description="Local time in ISO format")
|
||||
|
||||
|
||||
class FormattedTimeResponse(BaseModel):
|
||||
"""Response model for formatted time."""
|
||||
|
||||
formatted_time: str = Field(..., description="Formatted time string")
|
||||
|
||||
|
||||
class ConvertedTimeResponse(BaseModel):
|
||||
"""Response model for converted time."""
|
||||
|
||||
|
@ -80,3 +68,9 @@ class ParsedTimestampResponse(BaseModel):
|
|||
"""Response model for parsed timestamp."""
|
||||
|
||||
utc: str = Field(..., description="Parsed timestamp in UTC ISO format")
|
||||
|
||||
|
||||
class UnixToIsoResponse(BaseModel):
|
||||
"""Response model for Unix to ISO conversion."""
|
||||
|
||||
iso_time: str = Field(..., description="ISO formatted timestamp")
|
||||
|
|
|
@ -15,12 +15,11 @@ from .models import (
|
|||
ConvertTimeInput,
|
||||
ElapsedTimeInput,
|
||||
ElapsedTimeResponse,
|
||||
FormattedTimeResponse,
|
||||
FormatTimeInput,
|
||||
LocalTimeResponse,
|
||||
ParsedTimestampResponse,
|
||||
ParseTimestampInput,
|
||||
TimeResponse,
|
||||
UnixToIsoInput,
|
||||
UnixToIsoResponse,
|
||||
)
|
||||
|
||||
|
||||
|
@ -35,33 +34,48 @@ class TimeTool(BaseTool):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def get_current_utc() -> TimeResponse:
|
||||
"""Returns the current time in UTC in ISO format."""
|
||||
return TimeResponse(utc=datetime.now(tz=UTC).isoformat())
|
||||
def get_current(timezone: str = "UTC") -> TimeResponse:
|
||||
"""Get current time in specified timezone (defaults to UTC).
|
||||
|
||||
@staticmethod
|
||||
def get_current_local() -> LocalTimeResponse:
|
||||
"""Returns the current time in local timezone in ISO format."""
|
||||
return LocalTimeResponse(local_time=datetime.now(tz=UTC).isoformat())
|
||||
Args:
|
||||
timezone: IANA timezone name (e.g. 'UTC', 'America/New_York')
|
||||
|
||||
@staticmethod
|
||||
def format_current_time(data: FormatTimeInput) -> FormattedTimeResponse:
|
||||
"""Return the current time formatted for a specific timezone and format.
|
||||
Returns:
|
||||
TimeResponse: Current time in ISO format for specified timezone.
|
||||
|
||||
Raises:
|
||||
HTTPException: If timezone or format string is invalid.
|
||||
HTTPException: If timezone is invalid.
|
||||
"""
|
||||
try:
|
||||
tz = pytz.timezone(data.timezone)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Invalid timezone: {data.timezone}"
|
||||
) from None
|
||||
now = datetime.now(tz)
|
||||
try:
|
||||
return FormattedTimeResponse(formatted_time=now.strftime(data.format))
|
||||
tz = pytz.timezone(timezone)
|
||||
now = datetime.now(tz)
|
||||
return TimeResponse(utc=now.isoformat())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid format string: {e}") from e
|
||||
raise HTTPException(status_code=400, detail=f"Invalid timezone: {e}") from e
|
||||
|
||||
@staticmethod
|
||||
def unix_to_iso(data: UnixToIsoInput) -> UnixToIsoResponse:
|
||||
"""Convert Unix epoch timestamp to ISO format.
|
||||
|
||||
Args:
|
||||
data: Unix timestamp and optional timezone
|
||||
|
||||
Returns:
|
||||
UnixToIsoResponse: ISO formatted timestamp.
|
||||
|
||||
Raises:
|
||||
HTTPException: If timestamp or timezone is invalid.
|
||||
"""
|
||||
try:
|
||||
dt = datetime.fromtimestamp(data.timestamp, tz=UTC)
|
||||
if data.timezone and data.timezone != "UTC":
|
||||
target_tz = pytz.timezone(data.timezone)
|
||||
dt = dt.astimezone(target_tz)
|
||||
return UnixToIsoResponse(iso_time=dt.isoformat())
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Invalid timestamp or timezone: {e}"
|
||||
) from e
|
||||
|
||||
@staticmethod
|
||||
def convert_time(data: ConvertTimeInput) -> ConvertedTimeResponse:
|
||||
|
@ -143,54 +157,49 @@ class TimeTool(BaseTool):
|
|||
"""Return the FastAPI router for time tool endpoints."""
|
||||
router = APIRouter()
|
||||
|
||||
router.add_api_route(
|
||||
"/get_current_utc_time",
|
||||
self.get_current_utc,
|
||||
methods=["GET"],
|
||||
@router.get(
|
||||
"/get_time",
|
||||
response_model=TimeResponse,
|
||||
summary="Current UTC time",
|
||||
summary="Get current time in specified IANA timezone (defaults to UTC)",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/get_current_local_time",
|
||||
self.get_current_local,
|
||||
methods=["GET"],
|
||||
response_model=LocalTimeResponse,
|
||||
summary="Current Local Time",
|
||||
def get_time(timezone: str = "UTC") -> TimeResponse:
|
||||
return TimeTool.get_current_time(timezone)
|
||||
|
||||
@router.post(
|
||||
"/unix_to_iso",
|
||||
response_model=UnixToIsoResponse,
|
||||
summary="Convert Unix epoch timestamp to ISO format",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/format_time",
|
||||
self.format_current_time,
|
||||
methods=["POST"],
|
||||
response_model=FormattedTimeResponse,
|
||||
summary="Format current time",
|
||||
)
|
||||
router.add_api_route(
|
||||
def unix_to_iso(data: UnixToIsoInput) -> UnixToIsoResponse:
|
||||
return TimeTool.unix_to_iso(data)
|
||||
|
||||
@router.post(
|
||||
"/convert_time",
|
||||
self.convert_time,
|
||||
methods=["POST"],
|
||||
response_model=ConvertedTimeResponse,
|
||||
summary="Convert between timezones",
|
||||
summary="Convert timestamp from one timezone to another",
|
||||
)
|
||||
router.add_api_route(
|
||||
def convert_time(data: ConvertTimeInput) -> ConvertedTimeResponse:
|
||||
return TimeTool.convert_time(data)
|
||||
|
||||
@router.post(
|
||||
"/elapsed_time",
|
||||
self.elapsed_time,
|
||||
methods=["POST"],
|
||||
response_model=ElapsedTimeResponse,
|
||||
summary="Time elapsed between timestamps",
|
||||
summary="Calculate time difference between two timestamps",
|
||||
)
|
||||
router.add_api_route(
|
||||
def elapsed_time(data: ElapsedTimeInput) -> ElapsedTimeResponse:
|
||||
return TimeTool.elapsed_time(data)
|
||||
|
||||
@router.post(
|
||||
"/parse_timestamp",
|
||||
self.parse_timestamp,
|
||||
methods=["POST"],
|
||||
response_model=ParsedTimestampResponse,
|
||||
summary="Parse and normalize timestamps",
|
||||
)
|
||||
router.add_api_route(
|
||||
"/list_time_zones",
|
||||
self.list_time_zones,
|
||||
methods=["GET"],
|
||||
summary="All valid time zones",
|
||||
summary="Parse human-readable timestamp into standardized UTC format",
|
||||
)
|
||||
def parse_timestamp(data: ParseTimestampInput) -> ParsedTimestampResponse:
|
||||
return TimeTool.parse_timestamp(data)
|
||||
|
||||
@router.get("/list_time_zones", summary="Get list of all valid IANA timezone names")
|
||||
def list_time_zones() -> list[str]:
|
||||
return TimeTool.list_time_zones()
|
||||
|
||||
return router
|
||||
|
||||
|
|
|
@ -33,9 +33,9 @@ class WeatherTool(BaseTool):
|
|||
@router.get(
|
||||
"/forecast",
|
||||
response_model=WeatherForecastOutput,
|
||||
summary="Get current weather and forecast",
|
||||
summary="Get current weather conditions and hourly forecast by coordinates",
|
||||
)
|
||||
def get_weather_forecast(
|
||||
def forecast(
|
||||
latitude: Annotated[
|
||||
float | None, Query(description="Latitude for the location (e.g. 52.52)")
|
||||
] = None,
|
||||
|
|
|
@ -28,38 +28,13 @@ class WebTool(BaseTool):
|
|||
"""Return the FastAPI router for web tool endpoints."""
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/web_read", response_model=WebResponse)
|
||||
def web_read_get(
|
||||
url: Annotated[HttpUrl | None, Query(description="URL to parse")] = None,
|
||||
with_metadata: Annotated[bool, Query(description="Include metadata")] = True,
|
||||
include_formatting: Annotated[bool, Query(description="Keep formatting")] = True,
|
||||
include_images: Annotated[bool, Query(description="Include images")] = True,
|
||||
include_links: Annotated[bool, Query(description="Include links")] = True,
|
||||
include_tables: Annotated[bool, Query(description="Include tables")] = True,
|
||||
) -> WebResponse:
|
||||
"""Read web page content via GET request.
|
||||
|
||||
Returns:
|
||||
WebResponse: Parsed web content.
|
||||
|
||||
Raises:
|
||||
HTTPException: If URL is not provided.
|
||||
"""
|
||||
if not url:
|
||||
raise HTTPException(status_code=422, detail="URL query parameter is required.")
|
||||
|
||||
return WebTool._parse_web_page(
|
||||
str(url),
|
||||
with_metadata,
|
||||
include_formatting,
|
||||
include_images,
|
||||
include_links,
|
||||
include_tables,
|
||||
)
|
||||
|
||||
@router.post("/web_read", response_model=WebResponse)
|
||||
def web_read_post(request: WebRequest) -> WebResponse:
|
||||
"""Read web page content via POST request.
|
||||
@router.post(
|
||||
"/web_read",
|
||||
response_model=WebResponse,
|
||||
summary="Extract and parse webpage content into clean markdown",
|
||||
)
|
||||
def read(request: WebRequest) -> WebResponse:
|
||||
"""Extract and parse webpage content into clean markdown.
|
||||
|
||||
Returns:
|
||||
WebResponse: Parsed web content.
|
||||
|
@ -73,8 +48,12 @@ class WebTool(BaseTool):
|
|||
request.include_tables,
|
||||
)
|
||||
|
||||
@router.get("/web_raw", response_model=WebRawResponse)
|
||||
def web_raw(
|
||||
@router.get(
|
||||
"/web_raw",
|
||||
response_model=WebRawResponse,
|
||||
summary="Fetch raw HTML content and headers from any URL",
|
||||
)
|
||||
def raw(
|
||||
url: Annotated[
|
||||
HttpUrl | None, Query(description="URL to fetch raw content and headers from")
|
||||
] = None,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue