First version
This commit is contained in:
parent
4849c559dc
commit
39e26368e3
39 changed files with 2861 additions and 3 deletions
49
.forgejo/workflows/ci.yml
Normal file
49
.forgejo/workflows/ci.yml
Normal file
|
@ -0,0 +1,49 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and push Docker image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: https://github.com/docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Forgejo Container Registry
|
||||
uses: https://github.com/docker/login-action@v3
|
||||
with:
|
||||
registry: git.tomfos.tr
|
||||
username: ${{ gitea.repository_owner }}
|
||||
password: ${{ secrets.FORGEJO_TOKEN }}
|
||||
|
||||
- name: Determine image metadata
|
||||
id: meta
|
||||
uses: https://github.com/docker/metadata-action@v5
|
||||
with:
|
||||
images: git.tomfos.tr/${{ gitea.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch,if=ref==refs/heads/main,format=latest
|
||||
type=ref,event=branch,format={{ref_name}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: https://github.com/docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=git.tomfos.tr/${{ gitea.repository }}:build-cache
|
||||
cache-to: type=registry,ref=git.tomfos.tr/${{ gitea.repository }}:build-cache,mode=max
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -161,4 +161,3 @@ cython_debug/
|
|||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
|
|
47
Dockerfile
Normal file
47
Dockerfile
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Build stage using uv with a frozen lockfile and dependency caching
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS uv
|
||||
WORKDIR /app
|
||||
|
||||
# Enable bytecode compilation and copy mode
|
||||
ENV UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy
|
||||
|
||||
# Install dependencies using the lockfile
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --no-editable --no-install-project
|
||||
|
||||
# Install the project in a second layer
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev --no-editable
|
||||
|
||||
# Prepare runtime image
|
||||
FROM python:3.13-slim-bookworm AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# Install minimal system dependencies and create runtime user
|
||||
RUN apt-get update && apt-get install -y curl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd -g 1000 omcps \
|
||||
&& useradd -u 1000 -g 1000 -m omcps \
|
||||
&& mkdir -p /data \
|
||||
&& touch /data/memory.json \
|
||||
&& chown -R 1000:1000 /data
|
||||
|
||||
# Copy only the virtual environment from the build stage
|
||||
COPY --from=uv /app/.venv /app/.venv
|
||||
|
||||
# Switch to non-root user
|
||||
USER omcps
|
||||
|
||||
# Set environment variables for runtime
|
||||
ENV PATH="/app/.venv/bin:$PATH" \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
MEMORY_FILE_PATH="/data/memory.json"
|
||||
|
||||
# Use wrapper script to handle startup
|
||||
ENTRYPOINT ["uvicorn", "openapi_mcp_server.server:create_app", "--factory", "--host", "0.0.0.0", "--port", "80"]
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD curl -f http://localhost/health || exit 1
|
69
README.md
69
README.md
|
@ -1,3 +1,68 @@
|
|||
# openapi-mcp-server
|
||||
# OpenAPI MCP Server
|
||||
|
||||
MCP server to provide OpenAPI-flavour tools to an Open WebUI instance
|
||||
A unified OpenAPI-compatible server for the Multi-agent Conversation Platform (MCP) that provides
|
||||
various tools for Open WebUI integration. This single server exposes multiple tools through FastAPI
|
||||
endpoints with full OpenAPI documentation.
|
||||
|
||||
## Features
|
||||
|
||||
The server provides the following tool endpoints:
|
||||
|
||||
- **[Web Content Parsing](docs/web.md)** (`/web`) - Extract and parse web content using trafilatura
|
||||
- **[Memory/Storage](docs/memory.md)** (`/memory`) - Structured knowledge graph memory system
|
||||
- **[Time Operations](docs/time.md)** (`/time`) - Time utilities and timezone operations
|
||||
- **[Weather Information](docs/weather.md)** (`/weather`) - Real-time weather data and forecasts
|
||||
- **[Search](docs/searxng.md)** (`/searxng`) - Web search through SearXNG instances
|
||||
|
||||
Each tool provides detailed endpoint documentation - click the links above to learn about specific
|
||||
endpoints and their capabilities.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd openapi-mcp-server
|
||||
|
||||
# Start the server
|
||||
docker-compose up --build
|
||||
|
||||
# The server will be available at http://localhost:8080
|
||||
# OpenAPI documentation at http://localhost:8080/docs
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# Install dependencies using uv
|
||||
uv sync
|
||||
|
||||
# Run the development server
|
||||
uv run openapi-mcp-server
|
||||
|
||||
# The server will start on the default port with auto-reload
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
Once running, visit `/docs` for the interactive OpenAPI documentation where you can explore and test
|
||||
all available endpoints.
|
||||
|
||||
## Configuration
|
||||
|
||||
The server can be configured through environment variables:
|
||||
|
||||
- `MEMORY_FILE_PATH` - Path to the memory storage file (default: `/data/memory.json`)
|
||||
- Additional tool-specific configuration options are documented in the individual tool modules
|
||||
|
||||
## Architecture
|
||||
|
||||
This project follows a unified single-server architecture:
|
||||
|
||||
- **Single FastAPI application** serving multiple tool endpoints
|
||||
- **Modular tool design** with individual tools in `openapi_mcp_server/tools/`
|
||||
- **OpenAPI compliance** with full documentation and validation
|
||||
- **Docker-ready** with optimized multi-stage builds
|
||||
- **uv package management** for fast, reliable dependency handling
|
||||
|
|
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
services:
|
||||
tools:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- MEMORY_FILE_PATH=/data/memory.json
|
||||
- SEARXNG_BASE_URL=http://searxng:8080
|
||||
image: git.tomfos.tr/tom/openapi-mcp-server:latest
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
ports:
|
||||
- "8080:80"
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/data:rw
|
46
docs/memory.md
Normal file
46
docs/memory.md
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Memory Tool
|
||||
|
||||
A structured knowledge graph memory system that provides persistent storage for entities,
|
||||
relationships, and observations.
|
||||
|
||||
## Overview
|
||||
|
||||
The Memory tool provides a comprehensive knowledge graph system within the OpenAPI MCP Server
|
||||
framework. It allows you to store entities (people, places, concepts), create relationships between
|
||||
them, and add observations or facts about each entity.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/memory/create_entities` | Store new entities (people, places, concepts) with their properties and observations. Each entity has a name, type, and list of observations. |
|
||||
| `POST` | `/memory/create_relations` | Connect entities with srelationships (works_at, lives_in, knows, etc). Creates typed relationships between existing entities. |
|
||||
| `POST` | `/memory/add_observations` | Add new facts or observations to entities you've already stored. Extends existing entity knowledge without duplication. |
|
||||
| `POST` | `/memory/delete_entities` | Remove entities and all their connections from memory. Cleans up both the entity and any relationships involving it. |
|
||||
| `POST` | `/memory/delete_observations` | Remove specific facts or observations from entities. Allows selective cleanup of entity information. |
|
||||
| `POST` | `/memory/delete_relations` | Remove specific relationships between entities. Maintains entities while removing their connections. |
|
||||
| `GET` | `/memory/read_graph` | Retrieve all stored entities and relationships from memory. Returns the complete knowledge graph. |
|
||||
| `POST` | `/memory/search_nodes` | Find entities by searching names, types, or observations. Performs fuzzy search across all entity data. |
|
||||
| `POST` | `/memory/open_nodes` | Retrieve specific entities and their connections by exact name. Returns requested entities with their relationships. |
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Knowledge Graph Structure**: Entities connected by typed relationships
|
||||
- **Flexible Entity Types**: Store people, places, concepts, or any custom entity types
|
||||
- **Rich Observations**: Each entity can have multiple observations or facts
|
||||
- **Relationship Management**: Create, query, and delete relationships between entities
|
||||
- **Search Capabilities**: Find entities by name, type, or observation content
|
||||
- **Persistent Storage**: All data persisted to file system with atomic operations
|
||||
- **Duplicate Prevention**: Automatic deduplication of entities and relationships
|
||||
|
||||
## Data Model
|
||||
|
||||
- **Entity**: Has name, entity_type, and list of observations
|
||||
- **Relationship**: Connects two entities with a relationship type (from, to, relation_type)
|
||||
- **Knowledge Graph**: Collection of entities and their relationships
|
||||
|
||||
## Usage
|
||||
|
||||
The Memory tool is automatically loaded by the OpenAPI MCP Server framework. All endpoints are
|
||||
available under the `/memory` prefix when the server is running. Visit `/docs` for interactive API
|
||||
documentation.
|
55
docs/searxng.md
Normal file
55
docs/searxng.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# SearXNG Tool
|
||||
|
||||
A search proxy tool that provides search functionality through SearXNG search instances with OpenAPI
|
||||
compatibility.
|
||||
|
||||
## Overview
|
||||
|
||||
The SearXNG tool acts as a proxy server for SearXNG search instances, providing search capabilities
|
||||
within the OpenAPI MCP Server framework. It offers a standardized API interface for performing web
|
||||
searches across multiple search engines through SearXNG.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/searxng/search` | Search the web across multiple search engines. Accepts query, categories, engines, language, format, and pagination parameters. Returns structured search results with titles, URLs, content snippets, and metadata. |
|
||||
| `GET` | `/searxng/categories` | Get available search categories (general, images, news, etc). Returns list of categories that can be used to filter search results. |
|
||||
| `GET` | `/searxng/engines` | Get list of available search engines (Google, Bing, DuckDuckGo, etc). Returns engines that can be specified for targeted searches. |
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Multi-Engine Search**: Search across multiple search engines simultaneously
|
||||
- **Category Filtering**: Filter results by category (general, images, videos, news, etc.)
|
||||
- **Engine Selection**: Choose specific search engines for targeted results
|
||||
- **Structured Results**: Returns clean, structured search results with titles, URLs, and snippets
|
||||
- **Metadata Rich**: Includes search metadata like result count, suggestions, and engine info
|
||||
- **Pagination Support**: Navigate through multiple pages of search results
|
||||
- **Language Support**: Search in specific languages with language parameter
|
||||
|
||||
## Configuration
|
||||
|
||||
Configure the target SearXNG instance using the `SEARXNG_BASE_URL` environment variable:
|
||||
|
||||
- **Default**: `http://localhost:8080`
|
||||
- **Custom**: Set `SEARXNG_BASE_URL=https://your-searxng-instance.com`
|
||||
|
||||
## Search Parameters
|
||||
|
||||
- **query**: Search terms (required)
|
||||
- **categories**: Comma-separated category filters (optional)
|
||||
- **engines**: Comma-separated engine selection (optional)
|
||||
- **language**: Language code (default: "en")
|
||||
- **format**: Response format (default: "json")
|
||||
- **pageno**: Page number for pagination (default: 1)
|
||||
|
||||
## Usage
|
||||
|
||||
The SearXNG tool is automatically loaded by the OpenAPI MCP Server framework. All endpoints are
|
||||
available under the `/searxng` prefix when the server is running. Visit `/docs` for interactive API
|
||||
documentation.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Requires a running SearXNG instance. SearXNG is a free, open-source metasearch engine that aggregates
|
||||
results from multiple search engines without tracking users.
|
34
docs/time.md
Normal file
34
docs/time.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Time Tool
|
||||
|
||||
A comprehensive time utilities tool providing secure time operations, timezone conversion, and time
|
||||
calculations.
|
||||
|
||||
## Overview
|
||||
|
||||
The Time tool provides robust time-related functionality for the OpenAPI MCP Server, including
|
||||
current time retrieval, timezone conversions, Unix timestamp conversion, and timestamp parsing.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/time/get_time` | Get current time in any IANA timezone (defaults to UTC). Optional `timezone` parameter accepts IANA timezone names like 'America/New_York', 'Europe/London', etc. |
|
||||
| `POST` | `/time/unix_to_iso` | Convert Unix epoch timestamp to ISO format. Accepts Unix timestamp (float) and optional target timezone. |
|
||||
| `POST` | `/time/convert_time` | Convert timestamp from one timezone to another. Requires timestamp, source timezone, and target timezone. |
|
||||
| `POST` | `/time/elapsed_time` | Calculate time difference between two timestamps. Returns difference in specified units (seconds, minutes, hours, days). |
|
||||
| `POST` | `/time/parse_timestamp` | Parse flexible human-readable timestamps (e.g. 'June 1st 2024 3:30 PM', 'tomorrow at noon', '2024-06-01 15:30') into standardized UTC ISO format. |
|
||||
| `GET` | `/time/list_time_zones` | Get list of all valid IANA timezone names for use with other endpoints. |
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Flexible Input**: Supports various timestamp formats and human-readable date strings
|
||||
- **Timezone Aware**: Full IANA timezone support with automatic conversions
|
||||
- **Unix Compatibility**: Convert Unix epoch timestamps to readable ISO format
|
||||
- **Time Calculations**: Calculate elapsed time between any two timestamps
|
||||
- **Standardized Output**: All timestamps returned in ISO 8601 format
|
||||
|
||||
## Usage
|
||||
|
||||
The Time tool is automatically loaded by the OpenAPI MCP Server framework. All endpoints are
|
||||
available under the `/time` prefix when the server is running. Visit `/docs` for interactive API
|
||||
documentation.
|
44
docs/weather.md
Normal file
44
docs/weather.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Weather Tool
|
||||
|
||||
A weather information tool that provides real-time weather data and forecasts using the Open-Meteo API.
|
||||
|
||||
## Overview
|
||||
|
||||
The Weather tool provides weather information services within the OpenAPI MCP Server framework.
|
||||
It offers current weather conditions and hourly forecasts based on geographic coordinates using the
|
||||
reliable Open-Meteo weather API.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | `/weather/forecast` | Get current weather conditions and hourly forecast by coordinates. Requires latitude and longitude parameters. Temperature unit (Celsius/Fahrenheit) is automatically determined based on location. |
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Real-time Weather**: Current temperature and wind speed conditions
|
||||
- **Hourly Forecasts**: Detailed hourly weather data including temperature, humidity, and wind speed
|
||||
- **Smart Units**: Automatic temperature unit selection (Fahrenheit for US/Liberia/Myanmar, Celsius elsewhere)
|
||||
- **Geographic Accuracy**: Location-based weather using precise latitude/longitude coordinates
|
||||
- **Timezone Aware**: Weather data includes proper timezone information
|
||||
- **Reliable Source**: Uses Open-Meteo API for accurate, up-to-date weather information
|
||||
|
||||
## Data Structure
|
||||
|
||||
Weather responses include:
|
||||
|
||||
- **Current conditions**: Real-time temperature and wind speed
|
||||
- **Hourly forecasts**: Temperature, relative humidity, and wind speed by hour
|
||||
- **Location metadata**: Coordinates, timezone, and regional information
|
||||
- **Units**: Clear indication of temperature and measurement units
|
||||
|
||||
## Usage
|
||||
|
||||
The Weather tool is automatically loaded by the OpenAPI MCP Server framework. The forecast endpoint is
|
||||
available at `/weather/forecast` and requires latitude and longitude query parameters. Visit `/docs`
|
||||
for interactive API documentation.
|
||||
|
||||
## Data Source
|
||||
|
||||
Powered by the Open-Meteo API (<https://open-meteo.com>), providing reliable and free weather data
|
||||
without requiring API keys.
|
34
docs/web.md
Normal file
34
docs/web.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Web Tool
|
||||
|
||||
A web content parsing tool that extracts and processes web content using trafilatura for clean text
|
||||
extraction.
|
||||
|
||||
## Overview
|
||||
|
||||
The Web tool provides web content parsing and extraction capabilities within the OpenAPI MCP Server
|
||||
framework. It can fetch web pages and extract clean, readable content from HTML.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/web/web_read` | Extract and parse webpage content into clean markdown. Accepts URL and various formatting options (metadata, formatting, images, links, tables). Returns structured markdown content. |
|
||||
| `GET` | `/web/web_raw` | Fetch raw HTML content and headers from any URL. Returns unprocessed HTML content along with HTTP headers and status code for debugging or advanced processing. |
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Clean Content Extraction**: Uses trafilatura library for high-quality text extraction from web pages
|
||||
- **Markdown Output**: Converts web content to clean, readable markdown format
|
||||
- **Flexible Formatting**: Control inclusion of metadata, formatting, images, links, and tables
|
||||
- **Raw Content Access**: Get unprocessed HTML and headers when needed
|
||||
- **Error Handling**: Robust error handling for failed requests and invalid URLs
|
||||
|
||||
## Usage
|
||||
|
||||
The Web tool is automatically loaded by the OpenAPI MCP Server framework. All endpoints are
|
||||
available under the `/web` prefix when the server is running. Visit `/docs` for interactive API documentation.
|
||||
|
||||
## Technology
|
||||
|
||||
Built using trafilatura, a Python library specialized in extracting and processing web content,
|
||||
ensuring high-quality text extraction from web pages.
|
7
openapi_mcp_server/__init__.py
Normal file
7
openapi_mcp_server/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
"""OpenAPI MCP Server package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__title__ = "OpenAPI MCP Server"
|
||||
__description__ = "An OpenAPI-compatible server for the Multi-agent Conversation Platform (MCP)."
|
25
openapi_mcp_server/__main__.py
Normal file
25
openapi_mcp_server/__main__.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
"""Entry point for the OpenAPI MCP Server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uvicorn
|
||||
|
||||
from .core.config import config
|
||||
from .server import create_app
|
||||
|
||||
|
||||
def launch() -> None:
|
||||
"""Launch the OpenAPI MCP Server."""
|
||||
app = create_app()
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=config.host,
|
||||
port=config.port,
|
||||
reload=config.reload,
|
||||
log_level="debug" if config.debug else "info",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
launch()
|
3
openapi_mcp_server/core/__init__.py
Normal file
3
openapi_mcp_server/core/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""Core components for the OpenAPI MCP Server."""
|
||||
|
||||
from __future__ import annotations
|
29
openapi_mcp_server/core/config.py
Normal file
29
openapi_mcp_server/core/config.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
"""Configuration management for the OpenAPI MCP Server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class ServerConfig(BaseSettings):
|
||||
"""Server configuration settings."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
case_sensitive=False,
|
||||
)
|
||||
|
||||
host: str = Field(default="0.0.0.0", alias="HOST")
|
||||
port: int = Field(default=8000, alias="PORT")
|
||||
debug: bool = Field(default=False, alias="DEBUG")
|
||||
reload: bool = Field(default=False, alias="RELOAD")
|
||||
|
||||
# CORS settings
|
||||
cors_origins: list[str] = Field(default=["*"], alias="CORS_ORIGINS")
|
||||
cors_methods: list[str] = Field(default=["*"], alias="CORS_METHODS")
|
||||
cors_headers: list[str] = Field(default=["*"], alias="CORS_HEADERS")
|
||||
cors_credentials: bool = Field(default=True, alias="CORS_CREDENTIALS")
|
||||
|
||||
|
||||
config = ServerConfig()
|
23
openapi_mcp_server/core/middleware.py
Normal file
23
openapi_mcp_server/core/middleware.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
"""Middleware setup for the FastAPI application."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from .config import config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import FastAPI
|
||||
|
||||
|
||||
def setup_middleware(app: FastAPI) -> None:
|
||||
"""Setup middleware for the FastAPI application."""
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=config.cors_origins,
|
||||
allow_credentials=config.cors_credentials,
|
||||
allow_methods=config.cors_methods,
|
||||
allow_headers=config.cors_headers,
|
||||
)
|
67
openapi_mcp_server/core/registry.py
Normal file
67
openapi_mcp_server/core/registry.py
Normal file
|
@ -0,0 +1,67 @@
|
|||
"""Tool registry for dynamic discovery and registration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import FastAPI
|
||||
|
||||
from openapi_mcp_server.tools.base import BaseTool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Registry for managing tool modules and their routes."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the tool registry."""
|
||||
self.tools: dict[str, BaseTool] = {}
|
||||
|
||||
def discover_tools(self) -> None:
|
||||
"""Automatically discover and load all tool modules."""
|
||||
tools_path = Path(__file__).parent.parent / "tools"
|
||||
|
||||
for tool_dir in tools_path.iterdir():
|
||||
if (
|
||||
tool_dir.is_dir()
|
||||
and tool_dir.name not in {"__pycache__", "base.py"}
|
||||
and (tool_dir / "__init__.py").exists()
|
||||
and (tool_dir / "routes.py").exists()
|
||||
):
|
||||
self._load_tool(tool_dir.name)
|
||||
|
||||
def _load_tool(self, tool_name: str) -> None:
|
||||
"""Load a specific tool module."""
|
||||
try:
|
||||
# Import the tool's routes module
|
||||
module = importlib.import_module(f"openapi_mcp_server.tools.{tool_name}.routes")
|
||||
|
||||
# Look for a Tool class or get_tool function
|
||||
if hasattr(module, "tool"):
|
||||
tool = module.tool
|
||||
if isinstance(tool, BaseTool):
|
||||
self.tools[tool_name] = tool
|
||||
logger.info("Loaded tool: %s", tool_name)
|
||||
else:
|
||||
logger.warning("%s.tool is not a BaseTool instance", tool_name)
|
||||
else:
|
||||
logger.warning("%s module has no 'tool' attribute", tool_name)
|
||||
|
||||
except ImportError:
|
||||
logger.exception("Failed to load tool %s", tool_name)
|
||||
|
||||
def register_routes(self, app: FastAPI) -> None:
|
||||
"""Register all tool routes with the FastAPI application."""
|
||||
for tool_name, tool in self.tools.items():
|
||||
# Create a router with tool-specific prefix
|
||||
router = tool.get_router()
|
||||
app.include_router(router, prefix=f"/{tool_name}", tags=[tool_name])
|
||||
logger.info("Registered routes for tool: %s", tool_name)
|
||||
|
||||
|
||||
registry = ToolRegistry()
|
3
openapi_mcp_server/models/__init__.py
Normal file
3
openapi_mcp_server/models/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""Shared data models for the OpenAPI MCP Server."""
|
||||
|
||||
from __future__ import annotations
|
18
openapi_mcp_server/models/base.py
Normal file
18
openapi_mcp_server/models/base.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
"""Base data models for the OpenAPI MCP Server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Standard health check response."""
|
||||
|
||||
status: str = Field(default="healthy", description="Service health status")
|
||||
service: str = Field(..., description="Service name")
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Standard error response."""
|
||||
|
||||
detail: str = Field(..., description="Error message")
|
64
openapi_mcp_server/server.py
Normal file
64
openapi_mcp_server/server.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
"""Main FastAPI application factory."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from . import __description__, __title__, __version__
|
||||
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.
|
||||
|
||||
Returns:
|
||||
FastAPI: Configured FastAPI application instance.
|
||||
"""
|
||||
app = FastAPI(
|
||||
title=__title__,
|
||||
version=__version__,
|
||||
description=__description__,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
generate_unique_id_function=generate_unique_id,
|
||||
)
|
||||
|
||||
# Setup middleware
|
||||
setup_middleware(app)
|
||||
|
||||
# Add global health endpoint
|
||||
@app.get("/health", response_model=HealthResponse, tags=["health"])
|
||||
def health_check() -> HealthResponse:
|
||||
"""Global health check endpoint.
|
||||
|
||||
Returns:
|
||||
HealthResponse: Health status of the service.
|
||||
"""
|
||||
return HealthResponse(service="openapi-mcp-server")
|
||||
|
||||
# Discover and register tool routes
|
||||
registry.discover_tools()
|
||||
registry.register_routes(app)
|
||||
|
||||
return app
|
3
openapi_mcp_server/tools/__init__.py
Normal file
3
openapi_mcp_server/tools/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""Tool implementations for the OpenAPI MCP Server."""
|
||||
|
||||
from __future__ import annotations
|
30
openapi_mcp_server/tools/base.py
Normal file
30
openapi_mcp_server/tools/base.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
"""Base classes for MCP tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
class BaseTool(ABC):
|
||||
"""Base class for all MCP tools."""
|
||||
|
||||
def __init__(self, name: str, description: str) -> None:
|
||||
"""Initialize the tool with name and description."""
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
@abstractmethod
|
||||
def get_router(self) -> APIRouter:
|
||||
"""Return the FastAPI router for this tool."""
|
||||
|
||||
def get_health_endpoint(self) -> dict[str, str]:
|
||||
"""Standard health check response for this tool.
|
||||
|
||||
Returns:
|
||||
dict[str, str]: Health status dictionary.
|
||||
"""
|
||||
return {"status": "healthy", "service": self.name}
|
3
openapi_mcp_server/tools/memory/__init__.py
Normal file
3
openapi_mcp_server/tools/memory/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""Memory/storage functionality tool."""
|
||||
|
||||
from __future__ import annotations
|
80
openapi_mcp_server/tools/memory/models.py
Normal file
80
openapi_mcp_server/tools/memory/models.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
"""Data models for memory/storage tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Memory(BaseModel):
|
||||
"""A timestamped fact that references one or more entities."""
|
||||
|
||||
id: str = Field(..., description="Unique identifier for this memory")
|
||||
content: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"The detailed fact or information stored (comprehensive details for optimal recall)"
|
||||
),
|
||||
)
|
||||
entities: list[str] = Field(..., description="List of entity names this memory references")
|
||||
timestamp: str = Field(..., description="ISO timestamp when this memory was created")
|
||||
|
||||
|
||||
class Entity(BaseModel):
|
||||
"""Simple entity reference."""
|
||||
|
||||
name: str = Field(..., description="The name of the entity")
|
||||
entity_type: str = Field(default="general", description="The type of the entity")
|
||||
memory_count: int = Field(default=0, description="Number of memories referencing this entity")
|
||||
|
||||
|
||||
class MemoryGraph(BaseModel):
|
||||
"""Collection of memories and entities."""
|
||||
|
||||
memories: list[Memory]
|
||||
entities: list[Entity]
|
||||
|
||||
|
||||
class CreateMemoryRequest(BaseModel):
|
||||
"""Request to create a new memory."""
|
||||
|
||||
content: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"The detailed fact or information to store. Include comprehensive details, "
|
||||
"context, and specifics for best recall - avoid brief summaries."
|
||||
),
|
||||
)
|
||||
entities: list[str] = Field(..., description="List of entity names this memory references")
|
||||
|
||||
|
||||
class SearchMemoryRequest(BaseModel):
|
||||
"""Request to search memories."""
|
||||
|
||||
query: str = Field(..., description="Search term to find in memory content or entity names")
|
||||
limit: int = Field(default=10, description="Maximum number of memories to return")
|
||||
|
||||
|
||||
class GetEntityRequest(BaseModel):
|
||||
"""Request to get memories for specific entities."""
|
||||
|
||||
entities: list[str] = Field(..., description="List of entity names to retrieve memories for")
|
||||
limit: int = Field(default=5, description="Maximum number of memories to return")
|
||||
|
||||
|
||||
class DeleteMemoryRequest(BaseModel):
|
||||
"""Request to delete specific memories."""
|
||||
|
||||
memory_ids: list[str] = Field(..., description="List of memory IDs to delete")
|
||||
|
||||
|
||||
class MemorySummary(BaseModel):
|
||||
"""Summary statistics about stored memories."""
|
||||
|
||||
total_memories: int = Field(..., description="Total number of memories stored")
|
||||
total_entities: int = Field(..., description="Total number of unique entities")
|
||||
oldest_memory: str | None = Field(..., description="ISO timestamp of oldest memory")
|
||||
latest_memory: str | None = Field(..., description="ISO timestamp of latest memory")
|
||||
memory_timespan_days: int | None = Field(
|
||||
..., description="Days between oldest and latest memory"
|
||||
)
|
||||
top_entities: list[Entity] = Field(..., description="Most frequently referenced entities")
|
300
openapi_mcp_server/tools/memory/routes.py
Normal file
300
openapi_mcp_server/tools/memory/routes.py
Normal file
|
@ -0,0 +1,300 @@
|
|||
"""API routes for memory/storage tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from openapi_mcp_server.tools.base import BaseTool
|
||||
|
||||
from .models import (
|
||||
CreateMemoryRequest,
|
||||
DeleteMemoryRequest,
|
||||
Entity,
|
||||
GetEntityRequest,
|
||||
Memory,
|
||||
MemoryGraph,
|
||||
MemorySummary,
|
||||
SearchMemoryRequest,
|
||||
)
|
||||
from .storage import (
|
||||
generate_memory_id,
|
||||
get_current_timestamp,
|
||||
read_memory_graph,
|
||||
save_memory_graph,
|
||||
)
|
||||
|
||||
|
||||
class MemoryTool(BaseTool):
|
||||
"""Simplified memory system for storing timestamped facts."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the memory tool."""
|
||||
super().__init__(
|
||||
name="memory",
|
||||
description="A simple memory system for storing timestamped facts about entities",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_memory(req: CreateMemoryRequest) -> Memory:
|
||||
"""Store a new memory/fact.
|
||||
|
||||
Returns:
|
||||
Memory: The newly created memory with auto-generated timestamp and ID.
|
||||
"""
|
||||
graph = read_memory_graph()
|
||||
|
||||
# Create new memory with auto-generated timestamp and ID
|
||||
memory = Memory(
|
||||
id=generate_memory_id(),
|
||||
content=req.content,
|
||||
entities=req.entities,
|
||||
timestamp=get_current_timestamp(),
|
||||
)
|
||||
|
||||
graph.memories.append(memory)
|
||||
|
||||
# Update entity counts and ensure entities exist
|
||||
entity_dict = {e.name: e for e in graph.entities}
|
||||
for entity_name in req.entities:
|
||||
if entity_name in entity_dict:
|
||||
entity_dict[entity_name].memory_count += 1
|
||||
else:
|
||||
# Create new entity
|
||||
new_entity = Entity(name=entity_name, entity_type="general", memory_count=1)
|
||||
graph.entities.append(new_entity)
|
||||
entity_dict[entity_name] = new_entity
|
||||
|
||||
save_memory_graph(graph)
|
||||
return memory
|
||||
|
||||
@staticmethod
|
||||
def get_all_memories(limit: int = 20) -> MemoryGraph:
|
||||
"""Get all memories and entities.
|
||||
|
||||
Returns:
|
||||
MemoryGraph: All stored memories and entities, sorted by timestamp (newest first).
|
||||
"""
|
||||
graph = read_memory_graph()
|
||||
# Sort memories by timestamp (newest first)
|
||||
graph.memories.sort(key=lambda m: m.timestamp, reverse=True)
|
||||
if limit and len(graph.memories) > limit:
|
||||
graph.memories = graph.memories[:limit]
|
||||
return graph
|
||||
|
||||
@staticmethod
|
||||
def search_memories(req: SearchMemoryRequest) -> MemoryGraph:
|
||||
"""Search memories by content or entity names.
|
||||
|
||||
Returns:
|
||||
MemoryGraph: Filtered memories matching the search query.
|
||||
"""
|
||||
graph = read_memory_graph()
|
||||
query = req.query.lower()
|
||||
|
||||
matching_memories = []
|
||||
for memory in graph.memories:
|
||||
# Search in content
|
||||
if query in memory.content.lower():
|
||||
matching_memories.append(memory)
|
||||
continue
|
||||
# Search in entity names
|
||||
if any(query in entity.lower() for entity in memory.entities):
|
||||
matching_memories.append(memory)
|
||||
|
||||
# Sort by timestamp (newest first) and apply limit
|
||||
matching_memories.sort(key=lambda m: m.timestamp, reverse=True)
|
||||
if len(matching_memories) > req.limit:
|
||||
matching_memories = matching_memories[: req.limit]
|
||||
|
||||
# Get entities referenced in matching memories
|
||||
referenced_entities = set()
|
||||
for memory in matching_memories:
|
||||
referenced_entities.update(memory.entities)
|
||||
|
||||
matching_entities = [e for e in graph.entities if e.name in referenced_entities]
|
||||
|
||||
return MemoryGraph(memories=matching_memories, entities=matching_entities)
|
||||
|
||||
@staticmethod
|
||||
def get_entity_memories(req: GetEntityRequest) -> MemoryGraph:
|
||||
"""Get memories for specific entities.
|
||||
|
||||
Returns:
|
||||
MemoryGraph: Memories that reference the specified entities.
|
||||
"""
|
||||
graph = read_memory_graph()
|
||||
|
||||
# Check if memory references any of the requested entities
|
||||
matching_memories = [
|
||||
memory
|
||||
for memory in graph.memories
|
||||
if any(entity in memory.entities for entity in req.entities)
|
||||
]
|
||||
|
||||
# Sort by timestamp (newest first) and apply limit
|
||||
matching_memories.sort(key=lambda m: m.timestamp, reverse=True)
|
||||
if len(matching_memories) > req.limit:
|
||||
matching_memories = matching_memories[: req.limit]
|
||||
|
||||
# Get the requested entities
|
||||
matching_entities = [e for e in graph.entities if e.name in req.entities]
|
||||
|
||||
return MemoryGraph(memories=matching_memories, entities=matching_entities)
|
||||
|
||||
@staticmethod
|
||||
def delete_memories(req: DeleteMemoryRequest) -> dict[str, str]:
|
||||
"""Delete specific memories by ID.
|
||||
|
||||
Returns:
|
||||
dict: Success message with count of deleted memories.
|
||||
"""
|
||||
graph = read_memory_graph()
|
||||
original_count = len(graph.memories)
|
||||
|
||||
# Remove memories and track which entities were affected
|
||||
affected_entities = set()
|
||||
graph.memories = [
|
||||
m
|
||||
for m in graph.memories
|
||||
if m.id not in req.memory_ids or affected_entities.update(m.entities)
|
||||
]
|
||||
|
||||
deleted_count = original_count - len(graph.memories)
|
||||
|
||||
# Recalculate entity memory counts
|
||||
entity_counts = {}
|
||||
for memory in graph.memories:
|
||||
for entity_name in memory.entities:
|
||||
entity_counts[entity_name] = entity_counts.get(entity_name, 0) + 1
|
||||
|
||||
# Update entity counts and remove entities with zero memories
|
||||
graph.entities = [
|
||||
Entity(
|
||||
name=e.name, entity_type=e.entity_type, memory_count=entity_counts.get(e.name, 0)
|
||||
)
|
||||
for e in graph.entities
|
||||
if entity_counts.get(e.name, 0) > 0
|
||||
]
|
||||
|
||||
save_memory_graph(graph)
|
||||
return {"message": f"Deleted {deleted_count} memories"}
|
||||
|
||||
@staticmethod
|
||||
def get_summary() -> MemorySummary:
|
||||
"""Get summary statistics about stored memories.
|
||||
|
||||
Returns:
|
||||
MemorySummary: Statistics about memories and entities.
|
||||
"""
|
||||
graph = read_memory_graph()
|
||||
|
||||
if not graph.memories:
|
||||
return MemorySummary(
|
||||
total_memories=0,
|
||||
total_entities=0,
|
||||
oldest_memory=None,
|
||||
latest_memory=None,
|
||||
memory_timespan_days=None,
|
||||
top_entities=[],
|
||||
)
|
||||
|
||||
# Sort memories by timestamp
|
||||
sorted_memories = sorted(graph.memories, key=lambda m: m.timestamp)
|
||||
oldest = sorted_memories[0].timestamp
|
||||
latest = sorted_memories[-1].timestamp
|
||||
|
||||
# Calculate timespan
|
||||
try:
|
||||
oldest_dt = datetime.fromisoformat(oldest)
|
||||
latest_dt = datetime.fromisoformat(latest)
|
||||
timespan_days = (latest_dt - oldest_dt).days
|
||||
except ValueError:
|
||||
timespan_days = None
|
||||
|
||||
# Get top entities by memory count
|
||||
top_entities = sorted(graph.entities, key=lambda e: e.memory_count, reverse=True)[:10]
|
||||
|
||||
return MemorySummary(
|
||||
total_memories=len(graph.memories),
|
||||
total_entities=len(graph.entities),
|
||||
oldest_memory=oldest,
|
||||
latest_memory=latest,
|
||||
memory_timespan_days=timespan_days,
|
||||
top_entities=top_entities,
|
||||
)
|
||||
|
||||
def get_router(self) -> APIRouter:
|
||||
"""Return the FastAPI router for memory tool endpoints."""
|
||||
router = APIRouter()
|
||||
|
||||
router.add_api_route(
|
||||
"/create",
|
||||
MemoryTool.create_memory,
|
||||
methods=["POST"],
|
||||
response_model=Memory,
|
||||
summary=(
|
||||
"STORE MEMORY: Save detailed information about entities. Include comprehensive "
|
||||
"details, context, and specifics - avoid brief summaries."
|
||||
),
|
||||
)
|
||||
|
||||
router.add_api_route(
|
||||
"/all",
|
||||
MemoryTool.get_all_memories,
|
||||
methods=["GET"],
|
||||
response_model=MemoryGraph,
|
||||
summary=(
|
||||
"GET ALL MEMORIES: Retrieve all stored memories and entities, sorted by timestamp "
|
||||
"(newest first)."
|
||||
),
|
||||
)
|
||||
|
||||
router.add_api_route(
|
||||
"/search",
|
||||
MemoryTool.search_memories,
|
||||
methods=["POST"],
|
||||
response_model=MemoryGraph,
|
||||
summary=(
|
||||
"SEARCH MEMORIES: Find memories by searching content or entity names. Returns "
|
||||
"matching memories sorted by time."
|
||||
),
|
||||
)
|
||||
|
||||
router.add_api_route(
|
||||
"/entity",
|
||||
MemoryTool.get_entity_memories,
|
||||
methods=["POST"],
|
||||
response_model=MemoryGraph,
|
||||
summary=(
|
||||
"GET ENTITY MEMORIES: Retrieve all memories that reference specific entities, "
|
||||
"sorted by timestamp."
|
||||
),
|
||||
)
|
||||
|
||||
router.add_api_route(
|
||||
"/delete",
|
||||
MemoryTool.delete_memories,
|
||||
methods=["POST"],
|
||||
summary=(
|
||||
"DELETE MEMORIES: Remove specific memories by their IDs. Entity counts are "
|
||||
"automatically updated."
|
||||
),
|
||||
)
|
||||
|
||||
router.add_api_route(
|
||||
"/stats",
|
||||
MemoryTool.get_summary,
|
||||
methods=["GET"],
|
||||
response_model=MemorySummary,
|
||||
summary=(
|
||||
"MEMORY STATS: Get summary statistics - total memories, entities, timespan, and "
|
||||
"top entities by frequency."
|
||||
),
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
|
||||
tool = MemoryTool()
|
88
openapi_mcp_server/tools/memory/storage.py
Normal file
88
openapi_mcp_server/tools/memory/storage.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
"""Storage implementation for memory tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .models import Entity, Memory, MemoryGraph
|
||||
|
||||
MEMORY_FILE_PATH_ENV = os.getenv("MEMORY_FILE_PATH", "memory.json")
|
||||
MEMORY_FILE_PATH = Path(
|
||||
MEMORY_FILE_PATH_ENV
|
||||
if Path(MEMORY_FILE_PATH_ENV).is_absolute()
|
||||
else Path(__file__).parent / MEMORY_FILE_PATH_ENV
|
||||
)
|
||||
|
||||
|
||||
def read_memory_graph() -> MemoryGraph:
|
||||
"""Read the memory graph from file.
|
||||
|
||||
Returns:
|
||||
MemoryGraph: The memory graph loaded from storage.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the memory file is not found or permission is denied.
|
||||
"""
|
||||
if not MEMORY_FILE_PATH.exists():
|
||||
return MemoryGraph(memories=[], entities=[])
|
||||
|
||||
try:
|
||||
with MEMORY_FILE_PATH.open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
memories = [Memory(**m) for m in data.get("memories", [])]
|
||||
entities = [Entity(**e) for e in data.get("entities", [])]
|
||||
|
||||
return MemoryGraph(memories=memories, entities=entities)
|
||||
except PermissionError as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Permission denied when reading memory file: {MEMORY_FILE_PATH}",
|
||||
) from e
|
||||
except json.JSONDecodeError:
|
||||
# Handle legacy format or corrupted file
|
||||
return MemoryGraph(memories=[], entities=[])
|
||||
|
||||
|
||||
def save_memory_graph(graph: MemoryGraph) -> None:
|
||||
"""Save the memory graph to file.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the memory file is not found or permission is denied.
|
||||
"""
|
||||
data = {
|
||||
"memories": [m.model_dump() for m in graph.memories],
|
||||
"entities": [e.model_dump() for e in graph.entities],
|
||||
}
|
||||
|
||||
try:
|
||||
with MEMORY_FILE_PATH.open("w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
except PermissionError as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Permission denied when writing to memory file: {MEMORY_FILE_PATH}",
|
||||
) from e
|
||||
|
||||
|
||||
def generate_memory_id() -> str:
|
||||
"""Generate a unique ID for a memory.
|
||||
|
||||
Returns:
|
||||
str: Unique memory identifier with timestamp.
|
||||
"""
|
||||
return f"mem_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S_%f')}"
|
||||
|
||||
|
||||
def get_current_timestamp() -> str:
|
||||
"""Get current UTC timestamp in ISO format.
|
||||
|
||||
Returns:
|
||||
str: UTC timestamp in ISO format with Z suffix.
|
||||
"""
|
||||
return datetime.now(UTC).isoformat() + "Z"
|
3
openapi_mcp_server/tools/searxng/__init__.py
Normal file
3
openapi_mcp_server/tools/searxng/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""Search functionality via SearXNG."""
|
||||
|
||||
from __future__ import annotations
|
62
openapi_mcp_server/tools/searxng/models.py
Normal file
62
openapi_mcp_server/tools/searxng/models.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
"""Data models for SearXNG search tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
"""Request model for search operations."""
|
||||
|
||||
query: str = Field(..., description="Search query string")
|
||||
categories: str | None = Field(
|
||||
None, description="Comma-separated categories (e.g. 'general,web')"
|
||||
)
|
||||
engines: str | None = Field(None, description="Comma-separated search engines")
|
||||
language: str | None = Field("en", description="Language code (e.g. 'en', 'de')")
|
||||
format: str | None = Field("json", description="Response format (json, csv, rss)")
|
||||
pageno: int | None = Field(1, description="Page number")
|
||||
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
"""Individual search result model."""
|
||||
|
||||
title: str = Field(..., description="Result title")
|
||||
url: str = Field(..., description="Result URL")
|
||||
content: str | None = Field(None, description="Result content/snippet")
|
||||
engine: str = Field(..., description="Search engine that provided this result")
|
||||
category: str = Field(..., description="Category of the result")
|
||||
|
||||
|
||||
class SearchResponse(BaseModel):
|
||||
"""Response model for search operations."""
|
||||
|
||||
query: str = Field(..., description="Original search query")
|
||||
number_of_results: int = Field(..., description="Total number of results found")
|
||||
results: list[SearchResult] = Field(..., description="Search results")
|
||||
infoboxes: list[dict[str, Any]] | None = Field(None, description="Information boxes")
|
||||
suggestions: list[str] | None = Field(None, description="Search suggestions")
|
||||
engines: list[str] | None = Field(None, description="Engines used for search")
|
||||
|
||||
|
||||
class CategoriesResponse(BaseModel):
|
||||
"""Response model for available categories."""
|
||||
|
||||
categories: list[str] = Field(..., description="Available search categories")
|
||||
note: str | None = Field(None, description="Additional notes about the response")
|
||||
|
||||
|
||||
class EnginesResponse(BaseModel):
|
||||
"""Response model for available engines."""
|
||||
|
||||
engines: list[str] = Field(..., description="Available search engines")
|
||||
note: str | None = Field(None, description="Additional notes about the response")
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Health response model for SearXNG service."""
|
||||
|
||||
status: str = Field(..., description="Service health status")
|
||||
searxng_url: str = Field(..., description="SearXNG instance URL")
|
188
openapi_mcp_server/tools/searxng/routes.py
Normal file
188
openapi_mcp_server/tools/searxng/routes.py
Normal file
|
@ -0,0 +1,188 @@
|
|||
"""API routes for SearXNG search tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import requests
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from openapi_mcp_server.tools.base import BaseTool
|
||||
|
||||
from .models import (
|
||||
CategoriesResponse,
|
||||
EnginesResponse,
|
||||
SearchRequest,
|
||||
SearchResponse,
|
||||
SearchResult,
|
||||
)
|
||||
|
||||
SEARXNG_BASE_URL = os.getenv("SEARXNG_BASE_URL", "http://localhost:8080")
|
||||
|
||||
|
||||
class SearxngTool(BaseTool):
|
||||
"""SearXNG search proxy tool."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the searxng tool."""
|
||||
super().__init__(
|
||||
name="searxng",
|
||||
description="Proxy server for SearXNG search instances with OpenAPI compatibility",
|
||||
)
|
||||
|
||||
def get_router(self) -> APIRouter:
|
||||
"""Return the FastAPI router for searxng tool endpoints."""
|
||||
router = APIRouter()
|
||||
|
||||
@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.
|
||||
"""
|
||||
return SearxngTool._perform_search(
|
||||
request.query,
|
||||
request.categories,
|
||||
request.engines,
|
||||
request.language,
|
||||
request.format,
|
||||
request.pageno,
|
||||
)
|
||||
|
||||
@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:
|
||||
CategoriesResponse: Available search categories.
|
||||
"""
|
||||
try:
|
||||
response = requests.get(f"{SEARXNG_BASE_URL}/config", timeout=10)
|
||||
response.raise_for_status()
|
||||
config = response.json()
|
||||
|
||||
categories = config.get("categories", [])
|
||||
return CategoriesResponse(categories=categories or [])
|
||||
|
||||
except Exception as e:
|
||||
return CategoriesResponse(
|
||||
categories=[
|
||||
"general",
|
||||
"images",
|
||||
"videos",
|
||||
"news",
|
||||
"map",
|
||||
"music",
|
||||
"it",
|
||||
"science",
|
||||
"files",
|
||||
"social media",
|
||||
],
|
||||
note=f"Using default categories (SearXNG config unavailable: {e!s})",
|
||||
)
|
||||
|
||||
@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:
|
||||
EnginesResponse: Available search engines.
|
||||
"""
|
||||
try:
|
||||
response = requests.get(f"{SEARXNG_BASE_URL}/config", timeout=10)
|
||||
response.raise_for_status()
|
||||
config = response.json()
|
||||
|
||||
engines = config.get("engines", [])
|
||||
engine_names = [engine.get("name") for engine in engines if engine.get("name")]
|
||||
return EnginesResponse(engines=engine_names)
|
||||
|
||||
except Exception as e:
|
||||
return EnginesResponse(
|
||||
engines=[],
|
||||
note=f"Unable to fetch engines from SearXNG: {e!s}",
|
||||
)
|
||||
|
||||
return router
|
||||
|
||||
@staticmethod
|
||||
def _perform_search(
|
||||
query: str,
|
||||
categories: str | None = None,
|
||||
engines: str | None = None,
|
||||
language: str = "en",
|
||||
response_format: str = "json",
|
||||
pageno: int = 1,
|
||||
) -> SearchResponse:
|
||||
"""Internal function to perform the actual search.
|
||||
|
||||
Returns:
|
||||
SearchResponse: Search results from SearXNG.
|
||||
|
||||
Raises:
|
||||
HTTPException: If search fails or service is unavailable.
|
||||
"""
|
||||
params = {"q": query, "format": response_format, "lang": language, "pageno": pageno}
|
||||
|
||||
if categories:
|
||||
params["categories"] = categories
|
||||
|
||||
if engines:
|
||||
params["engines"] = engines
|
||||
|
||||
try:
|
||||
response = requests.get(f"{SEARXNG_BASE_URL}/search", params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
if response_format.lower() != "json":
|
||||
raise HTTPException(status_code=400, detail="Only JSON format is supported")
|
||||
|
||||
data = response.json()
|
||||
|
||||
results = []
|
||||
for result in data.get("results", []):
|
||||
search_result = SearchResult(
|
||||
title=result.get("title", ""),
|
||||
url=result.get("url", ""),
|
||||
content=result.get("content", result.get("snippet", "")),
|
||||
engine=result.get("engine", "unknown"),
|
||||
category=result.get("category", "general"),
|
||||
)
|
||||
results.append(search_result)
|
||||
|
||||
return SearchResponse(
|
||||
query=query,
|
||||
number_of_results=data.get("number_of_results", len(results)),
|
||||
results=results,
|
||||
infoboxes=data.get("infoboxes"),
|
||||
suggestions=data.get("suggestions"),
|
||||
engines=data.get("engines"),
|
||||
)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise HTTPException(
|
||||
status_code=503, detail=f"SearXNG service unavailable: {e!s}"
|
||||
) from e
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(
|
||||
status_code=502, detail=f"Invalid JSON response from SearXNG: {e!s}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Search error: {e!s}") from e
|
||||
|
||||
|
||||
tool = SearxngTool()
|
3
openapi_mcp_server/tools/time/__init__.py
Normal file
3
openapi_mcp_server/tools/time/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""Time-related operations tool."""
|
||||
|
||||
from __future__ import annotations
|
76
openapi_mcp_server/tools/time/models.py
Normal file
76
openapi_mcp_server/tools/time/models.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
"""Data models for time operations tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ConvertTimeInput(BaseModel):
|
||||
"""Input model for converting time between timezones."""
|
||||
|
||||
timestamp: str = Field(
|
||||
..., description="ISO 8601 formatted time string (e.g., 2024-01-01T12:00:00Z)"
|
||||
)
|
||||
from_tz: str = Field(
|
||||
..., description="Original IANA time zone of input (e.g. UTC or Europe/Berlin)"
|
||||
)
|
||||
to_tz: str = Field(..., description="Target IANA time zone to convert to")
|
||||
|
||||
|
||||
class ElapsedTimeInput(BaseModel):
|
||||
"""Input model for calculating elapsed time."""
|
||||
|
||||
start: str = Field(..., description="Start timestamp in ISO 8601 format")
|
||||
end: str = Field(..., description="End timestamp in ISO 8601 format")
|
||||
units: Literal["seconds", "minutes", "hours", "days"] = Field(
|
||||
"seconds", description="Unit for elapsed time"
|
||||
)
|
||||
|
||||
|
||||
class ParseTimestampInput(BaseModel):
|
||||
"""Input model for parsing timestamp strings."""
|
||||
|
||||
timestamp: str = Field(
|
||||
..., description="Flexible input timestamp string (e.g., 2024-06-01 12:00 PM)"
|
||||
)
|
||||
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 ConvertedTimeResponse(BaseModel):
|
||||
"""Response model for converted time."""
|
||||
|
||||
converted_time: str = Field(..., description="Converted time in ISO format")
|
||||
|
||||
|
||||
class ElapsedTimeResponse(BaseModel):
|
||||
"""Response model for elapsed time calculation."""
|
||||
|
||||
elapsed: float = Field(..., description="Elapsed time in specified units")
|
||||
unit: str = Field(..., description="Unit of elapsed time")
|
||||
|
||||
|
||||
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")
|
210
openapi_mcp_server/tools/time/routes.py
Normal file
210
openapi_mcp_server/tools/time/routes.py
Normal file
|
@ -0,0 +1,210 @@
|
|||
"""API routes for time operations tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytz
|
||||
from dateutil import parser as dateutil_parser
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from openapi_mcp_server.tools.base import BaseTool
|
||||
|
||||
from .models import (
|
||||
ConvertedTimeResponse,
|
||||
ConvertTimeInput,
|
||||
ElapsedTimeInput,
|
||||
ElapsedTimeResponse,
|
||||
ParsedTimestampResponse,
|
||||
ParseTimestampInput,
|
||||
TimeResponse,
|
||||
UnixToIsoInput,
|
||||
UnixToIsoResponse,
|
||||
)
|
||||
|
||||
|
||||
class TimeTool(BaseTool):
|
||||
"""Secure time utilities tool."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the time tool."""
|
||||
super().__init__(
|
||||
name="time",
|
||||
description="Provides secure UTC/local time retrieval and formatting",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_current(timezone: str = "UTC") -> TimeResponse:
|
||||
"""Get current time in specified timezone (defaults to UTC).
|
||||
|
||||
Args:
|
||||
timezone: IANA timezone name (e.g. 'UTC', 'America/New_York')
|
||||
|
||||
Returns:
|
||||
TimeResponse: Current time in ISO format for specified timezone.
|
||||
|
||||
Raises:
|
||||
HTTPException: If timezone is invalid.
|
||||
"""
|
||||
try:
|
||||
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 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:
|
||||
"""Convert a timestamp from one timezone to another.
|
||||
|
||||
Returns:
|
||||
ConvertedTimeResponse: Timestamp converted to target timezone.
|
||||
|
||||
Raises:
|
||||
HTTPException: If timezone or timestamp is invalid.
|
||||
"""
|
||||
try:
|
||||
from_zone = pytz.timezone(data.from_tz)
|
||||
to_zone = pytz.timezone(data.to_tz)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid timezone: {e}") from e
|
||||
|
||||
try:
|
||||
dt = dateutil_parser.parse(data.timestamp)
|
||||
dt = from_zone.localize(dt) if dt.tzinfo is None else dt.astimezone(from_zone)
|
||||
converted = dt.astimezone(to_zone)
|
||||
return ConvertedTimeResponse(converted_time=converted.isoformat())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid timestamp: {e}") from e
|
||||
|
||||
@staticmethod
|
||||
def elapsed_time(data: ElapsedTimeInput) -> ElapsedTimeResponse:
|
||||
"""Calculate the difference between two timestamps in chosen units.
|
||||
|
||||
Returns:
|
||||
ElapsedTimeResponse: Time difference in specified units.
|
||||
|
||||
Raises:
|
||||
HTTPException: If timestamps are invalid.
|
||||
"""
|
||||
try:
|
||||
start_dt = dateutil_parser.parse(data.start)
|
||||
end_dt = dateutil_parser.parse(data.end)
|
||||
delta = end_dt - start_dt
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid timestamps: {e}") from e
|
||||
|
||||
seconds = delta.total_seconds()
|
||||
result = {
|
||||
"seconds": seconds,
|
||||
"minutes": seconds / 60,
|
||||
"hours": seconds / 3600,
|
||||
"days": seconds / 86400,
|
||||
}
|
||||
|
||||
return ElapsedTimeResponse(elapsed=result[data.units], unit=data.units)
|
||||
|
||||
@staticmethod
|
||||
def parse_timestamp(data: ParseTimestampInput) -> ParsedTimestampResponse:
|
||||
"""Parse human-friendly input timestamp and return standardized UTC ISO time.
|
||||
|
||||
Returns:
|
||||
ParsedTimestampResponse: Standardized UTC timestamp.
|
||||
|
||||
Raises:
|
||||
HTTPException: If timestamp cannot be parsed.
|
||||
"""
|
||||
try:
|
||||
tz = pytz.timezone(data.timezone)
|
||||
dt = dateutil_parser.parse(data.timestamp)
|
||||
if dt.tzinfo is None:
|
||||
dt = tz.localize(dt)
|
||||
dt_utc = dt.astimezone(pytz.utc)
|
||||
return ParsedTimestampResponse(utc=dt_utc.isoformat())
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Could not parse: {e}") from e
|
||||
|
||||
@staticmethod
|
||||
def list_time_zones() -> list[str]:
|
||||
"""Return a list of all valid IANA time zones."""
|
||||
return list(pytz.all_timezones)
|
||||
|
||||
def get_router(self) -> APIRouter:
|
||||
"""Return the FastAPI router for time tool endpoints."""
|
||||
router = APIRouter()
|
||||
|
||||
@router.get(
|
||||
"/get_time",
|
||||
response_model=TimeResponse,
|
||||
summary="Get current time in specified IANA timezone (defaults to UTC)",
|
||||
)
|
||||
def get_time(timezone: str = "UTC") -> TimeResponse:
|
||||
return TimeTool.get_current(timezone)
|
||||
|
||||
@router.post(
|
||||
"/unix_to_iso",
|
||||
response_model=UnixToIsoResponse,
|
||||
summary="Convert Unix epoch timestamp to ISO format",
|
||||
)
|
||||
def unix_to_iso(data: UnixToIsoInput) -> UnixToIsoResponse:
|
||||
return TimeTool.unix_to_iso(data)
|
||||
|
||||
@router.post(
|
||||
"/convert_time",
|
||||
response_model=ConvertedTimeResponse,
|
||||
summary="Convert timestamp from one timezone to another",
|
||||
)
|
||||
def convert_time(data: ConvertTimeInput) -> ConvertedTimeResponse:
|
||||
return TimeTool.convert_time(data)
|
||||
|
||||
@router.post(
|
||||
"/elapsed_time",
|
||||
response_model=ElapsedTimeResponse,
|
||||
summary="Calculate time difference between two timestamps",
|
||||
)
|
||||
def elapsed_time(data: ElapsedTimeInput) -> ElapsedTimeResponse:
|
||||
return TimeTool.elapsed_time(data)
|
||||
|
||||
@router.post(
|
||||
"/parse_timestamp",
|
||||
response_model=ParsedTimestampResponse,
|
||||
summary=(
|
||||
"Parse flexible human-readable timestamps (e.g. 'June 1st 2024 3:30 PM', "
|
||||
"'tomorrow at noon', '2024-06-01 15:30') into standardized UTC ISO 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
|
||||
|
||||
|
||||
tool = TimeTool()
|
3
openapi_mcp_server/tools/weather/__init__.py
Normal file
3
openapi_mcp_server/tools/weather/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""Weather information tool."""
|
||||
|
||||
from __future__ import annotations
|
52
openapi_mcp_server/tools/weather/models.py
Normal file
52
openapi_mcp_server/tools/weather/models.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
"""Data models for weather information tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CurrentWeather(BaseModel):
|
||||
"""Current weather conditions model."""
|
||||
|
||||
time: str = Field(..., description="ISO 8601 format timestamp")
|
||||
temperature_2m: float = Field(
|
||||
...,
|
||||
alias="temperature_2m",
|
||||
description="Air temperature at 2 meters above ground",
|
||||
)
|
||||
wind_speed_10m: float = Field(
|
||||
..., alias="wind_speed_10m", description="Wind speed at 10 meters above ground"
|
||||
)
|
||||
|
||||
|
||||
class HourlyUnits(BaseModel):
|
||||
"""Units for hourly weather data."""
|
||||
|
||||
time: str
|
||||
temperature_2m: str
|
||||
relative_humidity_2m: str
|
||||
wind_speed_10m: str
|
||||
|
||||
|
||||
class HourlyData(BaseModel):
|
||||
"""Hourly weather data model."""
|
||||
|
||||
time: list[str]
|
||||
temperature_2m: list[float]
|
||||
relative_humidity_2m: list[int]
|
||||
wind_speed_10m: list[float]
|
||||
|
||||
|
||||
class WeatherForecastOutput(BaseModel):
|
||||
"""Complete weather forecast output model."""
|
||||
|
||||
latitude: float
|
||||
longitude: float
|
||||
generationtime_ms: float
|
||||
utc_offset_seconds: int
|
||||
timezone: str
|
||||
timezone_abbreviation: str
|
||||
elevation: float
|
||||
current: CurrentWeather = Field(..., description="Current weather conditions")
|
||||
hourly_units: HourlyUnits
|
||||
hourly: HourlyData
|
104
openapi_mcp_server/tools/weather/routes.py
Normal file
104
openapi_mcp_server/tools/weather/routes.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
"""API routes for weather information tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
import requests
|
||||
import reverse_geocoder as rg
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from openapi_mcp_server.tools.base import BaseTool
|
||||
|
||||
from .models import WeatherForecastOutput
|
||||
|
||||
OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
|
||||
FAHRENHEIT_COUNTRIES = {"US", "LR", "MM"}
|
||||
|
||||
|
||||
class WeatherTool(BaseTool):
|
||||
"""Weather forecast tool using Open-Meteo API."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the weather tool."""
|
||||
super().__init__(
|
||||
name="weather",
|
||||
description="Provides weather retrieval by latitude and longitude using Open-Meteo",
|
||||
)
|
||||
|
||||
def get_router(self) -> APIRouter:
|
||||
"""Return the FastAPI router for weather tool endpoints."""
|
||||
router = APIRouter()
|
||||
|
||||
@router.get(
|
||||
"/forecast",
|
||||
response_model=WeatherForecastOutput,
|
||||
summary="Get current weather conditions and hourly forecast by coordinates",
|
||||
)
|
||||
def forecast(
|
||||
latitude: Annotated[
|
||||
float | None, Query(description="Latitude for the location (e.g. 52.52)")
|
||||
] = None,
|
||||
longitude: Annotated[
|
||||
float | None, Query(description="Longitude for the location (e.g. 13.41)")
|
||||
] = None,
|
||||
) -> WeatherForecastOutput:
|
||||
"""Retrieves current weather conditions and hourly forecast for the given coordinates.
|
||||
|
||||
Temperature unit (Celsius/Fahrenheit) is determined automatically based on location.
|
||||
|
||||
Returns:
|
||||
WeatherForecastOutput: Current weather conditions and forecast data.
|
||||
|
||||
Raises:
|
||||
HTTPException: If weather API request fails or returns invalid data.
|
||||
"""
|
||||
if latitude is None or longitude is None:
|
||||
raise HTTPException(status_code=422, detail="Latitude and longitude are required.")
|
||||
# Determine temperature unit based on location
|
||||
try:
|
||||
geo_results = rg.search((latitude, longitude), mode=1)
|
||||
if geo_results:
|
||||
country_code = geo_results[0]["cc"]
|
||||
temperature_unit = (
|
||||
"fahrenheit" if country_code in FAHRENHEIT_COUNTRIES else "celsius"
|
||||
)
|
||||
else:
|
||||
temperature_unit = "celsius"
|
||||
except Exception:
|
||||
temperature_unit = "celsius"
|
||||
|
||||
params = {
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"current": "temperature_2m,wind_speed_10m",
|
||||
"hourly": "temperature_2m,relative_humidity_2m,wind_speed_10m",
|
||||
"timezone": "auto",
|
||||
"temperature_unit": temperature_unit,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(OPEN_METEO_URL, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if "current" not in data or "hourly" not in data:
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Unexpected response format from Open-Meteo API"
|
||||
)
|
||||
|
||||
return WeatherForecastOutput(**data)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise HTTPException(
|
||||
status_code=503, detail=f"Error connecting to Open-Meteo API: {e}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"An internal error occurred: {e}"
|
||||
) from e
|
||||
|
||||
return router
|
||||
|
||||
|
||||
tool = WeatherTool()
|
3
openapi_mcp_server/tools/web/__init__.py
Normal file
3
openapi_mcp_server/tools/web/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""Web content parsing tool."""
|
||||
|
||||
from __future__ import annotations
|
36
openapi_mcp_server/tools/web/models.py
Normal file
36
openapi_mcp_server/tools/web/models.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
"""Data models for web content parsing tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
|
||||
|
||||
class WebRequest(BaseModel):
|
||||
"""Request model for web content parsing."""
|
||||
|
||||
url: HttpUrl = Field(..., description="URL to parse and extract content from")
|
||||
favor_recall: bool | None = Field(True, description="Favor recall over precision in extraction")
|
||||
with_metadata: bool | None = Field(True, description="Include metadata in extraction")
|
||||
include_formatting: bool | None = Field(True, description="Keep formatting in output")
|
||||
include_images: bool | None = Field(True, description="Include images in output")
|
||||
include_links: bool | None = Field(True, description="Include links in output")
|
||||
include_tables: bool | None = Field(True, description="Include tables in output")
|
||||
|
||||
|
||||
class WebResponse(BaseModel):
|
||||
"""Response model for parsed web content."""
|
||||
|
||||
url: str = Field(..., description="Original URL that was parsed")
|
||||
content: str = Field(
|
||||
...,
|
||||
description="Extracted content in Markdown format with metadata frontmatter",
|
||||
)
|
||||
|
||||
|
||||
class WebRawResponse(BaseModel):
|
||||
"""Response model for raw web content."""
|
||||
|
||||
url: str = Field(..., description="Original URL that was fetched")
|
||||
status_code: int = Field(..., description="HTTP status code")
|
||||
headers: dict[str, str] = Field(..., description="Response headers")
|
||||
content: str = Field(..., description="Raw HTML content")
|
153
openapi_mcp_server/tools/web/routes.py
Normal file
153
openapi_mcp_server/tools/web/routes.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
"""API routes for web content parsing tool."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
import requests
|
||||
import trafilatura
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from pydantic import HttpUrl # noqa: TC002
|
||||
|
||||
from openapi_mcp_server.tools.base import BaseTool
|
||||
|
||||
from .models import WebRawResponse, WebRequest, WebResponse
|
||||
|
||||
|
||||
class WebTool(BaseTool):
|
||||
"""Web content parsing tool."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the web tool."""
|
||||
super().__init__(
|
||||
name="web",
|
||||
description="Extract and parse web page content into Markdown using trafilatura",
|
||||
)
|
||||
|
||||
def get_router(self) -> APIRouter:
|
||||
"""Return the FastAPI router for web tool endpoints."""
|
||||
router = APIRouter()
|
||||
|
||||
@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.
|
||||
"""
|
||||
return WebTool._parse_web_page(
|
||||
str(request.url),
|
||||
request.with_metadata,
|
||||
request.include_formatting,
|
||||
request.include_images,
|
||||
request.include_links,
|
||||
request.include_tables,
|
||||
)
|
||||
|
||||
@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,
|
||||
) -> WebRawResponse:
|
||||
"""Fetch raw HTML content and headers from URL.
|
||||
|
||||
Returns:
|
||||
WebRawResponse: Raw HTML content and headers.
|
||||
|
||||
Raises:
|
||||
HTTPException: If request fails or URL is invalid.
|
||||
"""
|
||||
if not url:
|
||||
raise HTTPException(status_code=422, detail="URL query parameter is required.")
|
||||
|
||||
try:
|
||||
response = requests.get(str(url), timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
headers = dict(response.headers)
|
||||
|
||||
return WebRawResponse(
|
||||
url=str(url),
|
||||
status_code=response.status_code,
|
||||
headers=headers,
|
||||
content=response.text,
|
||||
)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail=f"Unable to fetch content from URL: {e!s}",
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error fetching raw content: {e!s}"
|
||||
) from e
|
||||
|
||||
return router
|
||||
|
||||
@staticmethod
|
||||
def _parse_web_page(
|
||||
url: str,
|
||||
with_metadata: bool = True,
|
||||
include_formatting: bool = True,
|
||||
include_images: bool = True,
|
||||
include_links: bool = True,
|
||||
include_tables: bool = True,
|
||||
) -> WebResponse:
|
||||
"""Internal function to perform the actual web page parsing.
|
||||
|
||||
Returns:
|
||||
WebResponse: Parsed web content.
|
||||
|
||||
Raises:
|
||||
HTTPException: If parsing fails or URL is invalid.
|
||||
"""
|
||||
try:
|
||||
downloaded = trafilatura.fetch_url(url)
|
||||
if not downloaded:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"Unable to fetch content from URL: {url}"
|
||||
)
|
||||
|
||||
config = trafilatura.settings.use_config()
|
||||
config.set("DEFAULT", "EXTRACTION_TIMEOUT", "30")
|
||||
|
||||
extracted_content = trafilatura.extract(
|
||||
downloaded,
|
||||
output_format="markdown",
|
||||
favor_recall=True,
|
||||
with_metadata=with_metadata,
|
||||
include_formatting=include_formatting,
|
||||
include_images=include_images,
|
||||
include_links=include_links,
|
||||
include_tables=include_tables,
|
||||
config=config,
|
||||
)
|
||||
|
||||
if not extracted_content:
|
||||
raise HTTPException(
|
||||
status_code=422, detail=f"Unable to extract content from URL: {url}"
|
||||
)
|
||||
|
||||
return WebResponse(
|
||||
url=url,
|
||||
content=extracted_content,
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error parsing web page: {e!s}") from e
|
||||
|
||||
|
||||
# Create the tool instance that will be discovered by the registry
|
||||
tool = WebTool()
|
94
pyproject.toml
Normal file
94
pyproject.toml
Normal file
|
@ -0,0 +1,94 @@
|
|||
[project]
|
||||
name = "openapi-mcp-server"
|
||||
version = "0.1.0"
|
||||
description = "An OpenAPI-compatible server for the Multi-agent Conversation Platform (MCP)."
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
authors = [{ name = "Tom Foster", email = "tom@tcpip.uk" }]
|
||||
maintainers = [{ name = "Tom Foster", email = "tom@tcpip.uk" }]
|
||||
requires-python = ">=3.13"
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Framework :: AsyncIO",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"fastapi>=0",
|
||||
"pydantic>=2",
|
||||
"pydantic-settings>=2",
|
||||
"python-dateutil>=2",
|
||||
"python-multipart>=0",
|
||||
"pytz>=2025",
|
||||
"requests>=2",
|
||||
"reverse-geocoder>=1",
|
||||
"trafilatura>=2",
|
||||
"uvicorn[standard]>=0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://git.tomfos.tr/tom/openapi-mcp-server"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["ruff>=0"]
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project.scripts]
|
||||
openapi-mcp-server = "openapi_mcp_server.__main__:launch"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = { find = {} }
|
||||
|
||||
[tool.ruff]
|
||||
cache-dir = "/tmp/.ruff_cache"
|
||||
fix = true
|
||||
line-length = 100
|
||||
preview = true
|
||||
show-fixes = false
|
||||
target-version = "py313"
|
||||
unsafe-fixes = true
|
||||
|
||||
[tool.ruff.format]
|
||||
line-ending = "auto"
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
[tool.ruff.lint]
|
||||
fixable = ["ALL"]
|
||||
ignore = [
|
||||
"ANN401", # use of Any type
|
||||
"BLE001", # blind Exception usage
|
||||
"COM812", # missing trailing comma
|
||||
"CPY", # flake8-copyright
|
||||
"FBT", # boolean arguments
|
||||
"PLR0912", # too many branches
|
||||
"PLR0913", # too many arguments
|
||||
"PLR0915", # too many statements
|
||||
"PLR0917", # too many positional arguments
|
||||
"PLR6301", # method could be static
|
||||
"RUF029", # async methods that don't await
|
||||
"S104", # binding to all interfaces
|
||||
"S110", # passed exceptions
|
||||
"TRY301", # raise inside try block
|
||||
]
|
||||
select = ["ALL"]
|
||||
unfixable = [
|
||||
"F841", # local variable assigned but never used
|
||||
"RUF100", # unused noqa comments
|
||||
"T201", # don't strip print statement
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
combine-as-imports = true
|
||||
required-imports = ["from __future__ import annotations"]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
736
uv.lock
generated
Normal file
736
uv.lock
generated
Normal file
|
@ -0,0 +1,736 @@
|
|||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "babel"
|
||||
version = "2.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.6.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "courlan"
|
||||
version = "1.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "babel" },
|
||||
{ name = "tld" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/54/6d6ceeff4bed42e7a10d6064d35ee43a810e7b3e8beb4abeae8cff4713ae/courlan-1.3.2.tar.gz", hash = "sha256:0b66f4db3a9c39a6e22dd247c72cfaa57d68ea660e94bb2c84ec7db8712af190", size = 206382, upload-time = "2024-10-29T16:40:20.994Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/ca/6a667ccbe649856dcd3458bab80b016681b274399d6211187c6ab969fc50/courlan-1.3.2-py3-none-any.whl", hash = "sha256:d0dab52cf5b5b1000ee2839fbc2837e93b2514d3cb5bb61ae158a55b7a04c6be", size = 33848, upload-time = "2024-10-29T16:40:18.325Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dateparser"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pytz" },
|
||||
{ name = "regex" },
|
||||
{ name = "tzlocal" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/3f/d3207a05f5b6a78c66d86631e60bfba5af163738a599a5b9aa2c2737a09e/dateparser-1.2.1.tar.gz", hash = "sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3", size = 309924, upload-time = "2025-02-05T12:34:55.593Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/0a/981c438c4cd84147c781e4e96c1d72df03775deb1bc76c5a6ee8afa89c62/dateparser-1.2.1-py3-none-any.whl", hash = "sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c", size = 295658, upload-time = "2025-02-05T12:34:53.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "htmldate"
|
||||
version = "1.9.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "dateparser" },
|
||||
{ name = "lxml" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/26/aaae4cab984f0b7dd0f5f1b823fa2ed2fd4a2bb50acd5bd2f0d217562678/htmldate-1.9.3.tar.gz", hash = "sha256:ac0caf4628c3ded4042011e2d60dc68dfb314c77b106587dd307a80d77e708e9", size = 44913, upload-time = "2024-12-30T12:52:35.206Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/05/49/8872130016209c20436ce0c1067de1cf630755d0443d068a5bc17fa95015/htmldate-1.9.3-py3-none-any.whl", hash = "sha256:3fadc422cf3c10a5cdb5e1b914daf37ec7270400a80a1b37e2673ff84faaaff8", size = 31565, upload-time = "2024-12-30T12:52:32.145Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httptools"
|
||||
version = "0.6.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "justext"
|
||||
version = "3.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lxml", extra = ["html-clean"] },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/f3/45890c1b314f0d04e19c1c83d534e611513150939a7cf039664d9ab1e649/justext-3.0.2.tar.gz", hash = "sha256:13496a450c44c4cd5b5a75a5efcd9996066d2a189794ea99a49949685a0beb05", size = 828521, upload-time = "2025-02-25T20:21:49.934Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/ac/52f4e86d1924a7fc05af3aeb34488570eccc39b4af90530dd6acecdf16b5/justext-3.0.2-py2.py3-none-any.whl", hash = "sha256:62b1c562b15c3c6265e121cc070874243a443bfd53060e869393f09d6b6cc9a7", size = 837940, upload-time = "2025-02-25T20:21:44.179Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "5.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
html-clean = [
|
||||
{ name = "lxml-html-clean" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml-html-clean"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "lxml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/b6/466e71db127950fb8d172026a8f0a9f0dc6f64c8e78e2ca79f252e5790b8/lxml_html_clean-0.4.2.tar.gz", hash = "sha256:91291e7b5db95430abf461bc53440964d58e06cc468950f9e47db64976cebcb3", size = 21622, upload-time = "2025-04-09T11:33:59.432Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/0b/942cb7278d6caad79343ad2ddd636ed204a47909b969d19114a3097f5aa3/lxml_html_clean-0.4.2-py3-none-any.whl", hash = "sha256:74ccfba277adcfea87a1e9294f47dd86b05d65b4da7c5b07966e3d5f3be8a505", size = 14184, upload-time = "2025-04-09T11:33:57.988Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/db/8e12381333aea300890829a0a36bfa738cac95475d88982d538725143fd9/numpy-2.3.0.tar.gz", hash = "sha256:581f87f9e9e9db2cba2141400e160e9dd644ee248788d6f90636eeb8fd9260a6", size = 20382813, upload-time = "2025-06-07T14:54:32.608Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/fc/1d67f751fd4dbafc5780244fe699bc4084268bad44b7c5deb0492473127b/numpy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5754ab5595bfa2c2387d241296e0381c21f44a4b90a776c3c1d39eede13a746a", size = 20889633, upload-time = "2025-06-07T14:44:06.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/95/73ffdb69e5c3f19ec4530f8924c4386e7ba097efc94b9c0aff607178ad94/numpy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d11fa02f77752d8099573d64e5fe33de3229b6632036ec08f7080f46b6649959", size = 14151683, upload-time = "2025-06-07T14:44:28.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/d5/06d4bb31bb65a1d9c419eb5676173a2f90fd8da3c59f816cc54c640ce265/numpy-2.3.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:aba48d17e87688a765ab1cd557882052f238e2f36545dfa8e29e6a91aef77afe", size = 5102683, upload-time = "2025-06-07T14:44:38.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/8b/6c2cef44f8ccdc231f6b56013dff1d71138c48124334aded36b1a1b30c5a/numpy-2.3.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4dc58865623023b63b10d52f18abaac3729346a7a46a778381e0e3af4b7f3beb", size = 6640253, upload-time = "2025-06-07T14:44:49.359Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/aa/fca4bf8de3396ddb59544df9b75ffe5b73096174de97a9492d426f5cd4aa/numpy-2.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df470d376f54e052c76517393fa443758fefcdd634645bc9c1f84eafc67087f0", size = 14258658, upload-time = "2025-06-07T14:45:10.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/12/734dce1087eed1875f2297f687e671cfe53a091b6f2f55f0c7241aad041b/numpy-2.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:87717eb24d4a8a64683b7a4e91ace04e2f5c7c77872f823f02a94feee186168f", size = 16628765, upload-time = "2025-06-07T14:45:35.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/03/ffa41ade0e825cbcd5606a5669962419528212a16082763fc051a7247d76/numpy-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fa264d56882b59dcb5ea4d6ab6f31d0c58a57b41aec605848b6eb2ef4a43e8", size = 15564335, upload-time = "2025-06-07T14:45:58.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/58/869398a11863310aee0ff85a3e13b4c12f20d032b90c4b3ee93c3b728393/numpy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e651756066a0eaf900916497e20e02fe1ae544187cb0fe88de981671ee7f6270", size = 18360608, upload-time = "2025-06-07T14:46:25.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/8a/5756935752ad278c17e8a061eb2127c9a3edf4ba2c31779548b336f23c8d/numpy-2.3.0-cp313-cp313-win32.whl", hash = "sha256:e43c3cce3b6ae5f94696669ff2a6eafd9a6b9332008bafa4117af70f4b88be6f", size = 6310005, upload-time = "2025-06-07T14:50:13.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/60/61d60cf0dfc0bf15381eaef46366ebc0c1a787856d1db0c80b006092af84/numpy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:81ae0bf2564cf475f94be4a27ef7bcf8af0c3e28da46770fc904da9abd5279b5", size = 12729093, upload-time = "2025-06-07T14:50:31.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/31/2f2f2d2b3e3c32d5753d01437240feaa32220b73258c9eef2e42a0832866/numpy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:c8738baa52505fa6e82778580b23f945e3578412554d937093eac9205e845e6e", size = 9885689, upload-time = "2025-06-07T14:50:47.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/89/c7828f23cc50f607ceb912774bb4cff225ccae7131c431398ad8400e2c98/numpy-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39b27d8b38942a647f048b675f134dd5a567f95bfff481f9109ec308515c51d8", size = 20986612, upload-time = "2025-06-07T14:46:56.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/46/79ecf47da34c4c50eedec7511e53d57ffdfd31c742c00be7dc1d5ffdb917/numpy-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0eba4a1ea88f9a6f30f56fdafdeb8da3774349eacddab9581a21234b8535d3d3", size = 14298953, upload-time = "2025-06-07T14:47:18.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/44/f6caf50713d6ff4480640bccb2a534ce1d8e6e0960c8f864947439f0ee95/numpy-2.3.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0f1f11d0a1da54927436505a5a7670b154eac27f5672afc389661013dfe3d4f", size = 5225806, upload-time = "2025-06-07T14:47:27.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/43/e1fd1aca7c97e234dd05e66de4ab7a5be54548257efcdd1bc33637e72102/numpy-2.3.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:690d0a5b60a47e1f9dcec7b77750a4854c0d690e9058b7bef3106e3ae9117808", size = 6735169, upload-time = "2025-06-07T14:47:38.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/89/f76f93b06a03177c0faa7ca94d0856c4e5c4bcaf3c5f77640c9ed0303e1c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8b51ead2b258284458e570942137155978583e407babc22e3d0ed7af33ce06f8", size = 14330701, upload-time = "2025-06-07T14:47:59.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/f5/4858c3e9ff7a7d64561b20580cf7cc5d085794bd465a19604945d6501f6c/numpy-2.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:aaf81c7b82c73bd9b45e79cfb9476cb9c29e937494bfe9092c26aece812818ad", size = 16692983, upload-time = "2025-06-07T14:48:24.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/17/0e3b4182e691a10e9483bcc62b4bb8693dbf9ea5dc9ba0b77a60435074bb/numpy-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f420033a20b4f6a2a11f585f93c843ac40686a7c3fa514060a97d9de93e5e72b", size = 15641435, upload-time = "2025-06-07T14:48:47.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/d5/463279fda028d3c1efa74e7e8d507605ae87f33dbd0543cf4c4527c8b882/numpy-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d344ca32ab482bcf8735d8f95091ad081f97120546f3d250240868430ce52555", size = 18433798, upload-time = "2025-06-07T14:49:14.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/1e/7a9d98c886d4c39a2b4d3a7c026bffcf8fbcaf518782132d12a301cfc47a/numpy-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:48a2e8eaf76364c32a1feaa60d6925eaf32ed7a040183b807e02674305beef61", size = 6438632, upload-time = "2025-06-07T14:49:25.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ab/66fc909931d5eb230107d016861824f335ae2c0533f422e654e5ff556784/numpy-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ba17f93a94e503551f154de210e4d50c5e3ee20f7e7a1b5f6ce3f22d419b93bb", size = 12868491, upload-time = "2025-06-07T14:49:44.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/e8/2c8a1c9e34d6f6d600c83d5ce5b71646c32a13f34ca5c518cc060639841c/numpy-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f14e016d9409680959691c109be98c436c6249eaf7f118b424679793607b5944", size = 9935345, upload-time = "2025-06-07T14:50:02.311Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openapi-mcp-server"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "pytz" },
|
||||
{ name = "requests" },
|
||||
{ name = "reverse-geocoder" },
|
||||
{ name = "trafilatura" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "fastapi", specifier = ">=0" },
|
||||
{ name = "pydantic", specifier = ">=2" },
|
||||
{ name = "pydantic-settings", specifier = ">=2" },
|
||||
{ name = "python-dateutil", specifier = ">=2" },
|
||||
{ name = "python-multipart", specifier = ">=0" },
|
||||
{ name = "pytz", specifier = ">=2025" },
|
||||
{ name = "requests", specifier = ">=2" },
|
||||
{ name = "reverse-geocoder", specifier = ">=1" },
|
||||
{ name = "trafilatura", specifier = ">=2" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0" }]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.33.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2024.11.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reverse-geocoder"
|
||||
version = "1.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "scipy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/0f/b7d5d4b36553731f11983e19e1813a1059ad0732c5162c01b3220c927d31/reverse_geocoder-1.5.1.tar.gz", hash = "sha256:2a2e781b5f69376d922b78fe8978f1350c84fce0ddb07e02c834ecf98b57c75c", size = 2246559, upload-time = "2016-09-15T16:46:46.277Z" }
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scipy"
|
||||
version = "1.15.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.46.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tld"
|
||||
version = "0.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/a1/5723b07a70c1841a80afc9ac572fdf53488306848d844cd70519391b0d26/tld-0.13.1.tar.gz", hash = "sha256:75ec00936cbcf564f67361c41713363440b6c4ef0f0c1592b5b0fbe72c17a350", size = 462000, upload-time = "2025-05-21T22:18:29.341Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/70/b2f38360c3fc4bc9b5e8ef429e1fde63749144ac583c2dbdf7e21e27a9ad/tld-0.13.1-py2.py3-none-any.whl", hash = "sha256:a2d35109433ac83486ddf87e3c4539ab2c5c2478230e5d9c060a18af4b03aa7c", size = 274718, upload-time = "2025-05-21T22:18:25.811Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "trafilatura"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "courlan" },
|
||||
{ name = "htmldate" },
|
||||
{ name = "justext" },
|
||||
{ name = "lxml" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/25/e3ebeefdebfdfae8c4a4396f5a6ea51fc6fa0831d63ce338e5090a8003dc/trafilatura-2.0.0.tar.gz", hash = "sha256:ceb7094a6ecc97e72fea73c7dba36714c5c5b577b6470e4520dca893706d6247", size = 253404, upload-time = "2024-12-03T15:23:24.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/b6/097367f180b6383a3581ca1b86fcae284e52075fa941d1232df35293363c/trafilatura-2.0.0-py3-none-any.whl", hash = "sha256:77eb5d1e993747f6f20938e1de2d840020719735690c840b9a1024803a4cd51d", size = 132557, upload-time = "2024-12-03T15:23:21.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "httptools" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
|
||||
{ name = "watchfiles" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvloop"
|
||||
version = "0.21.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789, upload-time = "2025-06-15T19:05:42.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551, upload-time = "2025-06-15T19:05:43.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420, upload-time = "2025-06-15T19:05:45.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950, upload-time = "2025-06-15T19:05:46.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706, upload-time = "2025-06-15T19:05:47.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814, upload-time = "2025-06-15T19:05:48.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820, upload-time = "2025-06-15T19:05:50.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194, upload-time = "2025-06-15T19:05:51.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349, upload-time = "2025-06-15T19:05:52.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836, upload-time = "2025-06-15T19:05:53.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343, upload-time = "2025-06-15T19:05:54.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916, upload-time = "2025-06-15T19:05:55.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582, upload-time = "2025-06-15T19:05:56.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752, upload-time = "2025-06-15T19:05:57.359Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436, upload-time = "2025-06-15T19:05:58.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016, upload-time = "2025-06-15T19:05:59.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727, upload-time = "2025-06-15T19:06:01.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/00/70f75c47f05dea6fd30df90f047765f6fc2d6eb8b5a3921379b0b04defa2/watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297", size = 402114, upload-time = "2025-06-15T19:06:06.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/03/acd69c48db4a1ed1de26b349d94077cca2238ff98fd64393f3e97484cae6/watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018", size = 393879, upload-time = "2025-06-15T19:06:07.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/c8/a9a2a6f9c8baa4eceae5887fecd421e1b7ce86802bcfc8b6a942e2add834/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0", size = 450026, upload-time = "2025-06-15T19:06:08.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/51/d572260d98388e6e2b967425c985e07d47ee6f62e6455cefb46a6e06eda5/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12", size = 457917, upload-time = "2025-06-15T19:06:09.988Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/2d/4258e52917bf9f12909b6ec314ff9636276f3542f9d3807d143f27309104/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb", size = 483602, upload-time = "2025-06-15T19:06:11.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/99/bee17a5f341a4345fe7b7972a475809af9e528deba056f8963d61ea49f75/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77", size = 596758, upload-time = "2025-06-15T19:06:12.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/76/e4bec1d59b25b89d2b0716b41b461ed655a9a53c60dc78ad5771fda5b3e6/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92", size = 477601, upload-time = "2025-06-15T19:06:13.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/fa/a514292956f4a9ce3c567ec0c13cce427c158e9f272062685a8a727d08fc/watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e", size = 451936, upload-time = "2025-06-15T19:06:14.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/5d/c3bf927ec3bbeb4566984eba8dd7a8eb69569400f5509904545576741f88/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b", size = 626243, upload-time = "2025-06-15T19:06:16.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/65/6e12c042f1a68c556802a84d54bb06d35577c81e29fba14019562479159c/watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259", size = 623073, upload-time = "2025-06-15T19:06:17.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/ab/7f79d9bf57329e7cbb0a6fd4c7bd7d0cee1e4a8ef0041459f5409da3506c/watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f", size = 400872, upload-time = "2025-06-15T19:06:18.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d5/3f7bf9912798e9e6c516094db6b8932df53b223660c781ee37607030b6d3/watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e", size = 392877, upload-time = "2025-06-15T19:06:19.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/c5/54ec7601a2798604e01c75294770dbee8150e81c6e471445d7601610b495/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa", size = 449645, upload-time = "2025-06-15T19:06:20.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/04/c2f44afc3b2fce21ca0b7802cbd37ed90a29874f96069ed30a36dfe57c2b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8", size = 457424, upload-time = "2025-06-15T19:06:21.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/b0/eec32cb6c14d248095261a04f290636da3df3119d4040ef91a4a50b29fa5/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f", size = 481584, upload-time = "2025-06-15T19:06:22.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/e2/ca4bb71c68a937d7145aa25709e4f5d68eb7698a25ce266e84b55d591bbd/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e", size = 596675, upload-time = "2025-06-15T19:06:24.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/dd/b0e4b7fb5acf783816bc950180a6cd7c6c1d2cf7e9372c0ea634e722712b/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb", size = 477363, upload-time = "2025-06-15T19:06:25.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240, upload-time = "2025-06-15T19:06:26.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607, upload-time = "2025-06-15T19:06:27.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315, upload-time = "2025-06-15T19:06:29.076Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||
]
|
Loading…
Add table
Add a link
Reference in a new issue