Upload 14 files
Browse files- Dockerfile +13 -8
- README.md +67 -5
- app.py +51 -33
- dateutil/mcp_output/README_MCP.md +89 -129
- dateutil/mcp_output/mcp_plugin/adapter.py +200 -424
- dateutil/mcp_output/mcp_plugin/main.py +3 -10
- dateutil/mcp_output/mcp_plugin/mcp_service.py +224 -188
- dateutil/mcp_output/requirements.txt +2 -7
- dateutil/mcp_output/start_mcp.py +21 -18
- port.json +1 -5
- requirements.txt +3 -4
- run_docker.ps1 +6 -24
- run_docker.sh +6 -73
Dockerfile
CHANGED
|
@@ -1,18 +1,23 @@
|
|
| 1 |
-
FROM python:3.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
|
| 7 |
WORKDIR /app
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
COPY --chown=user . /app
|
| 13 |
ENV MCP_TRANSPORT=http
|
| 14 |
ENV MCP_PORT=7860
|
| 15 |
|
| 16 |
EXPOSE 7860
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 4 |
+
PYTHONUNBUFFERED=1
|
|
|
|
| 5 |
|
| 6 |
WORKDIR /app
|
| 7 |
|
| 8 |
+
RUN groupadd -g 1000 appuser && useradd -m -u 1000 -g 1000 appuser
|
| 9 |
+
|
| 10 |
+
COPY requirements.txt /app/requirements.txt
|
| 11 |
+
RUN pip install --no-cache-dir -r /app/requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY dateutil /app/dateutil
|
| 14 |
+
COPY app.py /app/app.py
|
| 15 |
|
|
|
|
| 16 |
ENV MCP_TRANSPORT=http
|
| 17 |
ENV MCP_PORT=7860
|
| 18 |
|
| 19 |
EXPOSE 7860
|
| 20 |
|
| 21 |
+
USER appuser
|
| 22 |
+
|
| 23 |
+
ENTRYPOINT ["python", "dateutil/mcp_output/start_mcp.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,72 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: dateutil MCP Service
|
| 3 |
+
emoji: 🔧
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# dateutil MCP Service
|
| 12 |
+
|
| 13 |
+
This deployment packages `python-dateutil` as an MCP service using FastMCP.
|
| 14 |
+
It supports local `stdio` mode and Docker/HTTP mode for HuggingFace Spaces.
|
| 15 |
+
|
| 16 |
+
## Available MCP Tools
|
| 17 |
+
|
| 18 |
+
- `health_check`
|
| 19 |
+
- `parse_datetime`
|
| 20 |
+
- `parse_iso_datetime`
|
| 21 |
+
- `compute_relativedelta`
|
| 22 |
+
- `generate_rrule_occurrences`
|
| 23 |
+
- `resolve_timezone`
|
| 24 |
+
- `calculate_easter`
|
| 25 |
+
- `list_loaded_modules`
|
| 26 |
+
|
| 27 |
+
## Local stdio usage
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
cd dateutil/mcp_output
|
| 31 |
+
python start_mcp.py
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Or explicitly:
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
MCP_TRANSPORT=stdio MCP_PORT=8000 python start_mcp.py
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
For local CLI / Claude Desktop style clients, use stdio transport.
|
| 41 |
+
|
| 42 |
+
## HTTP client usage
|
| 43 |
+
|
| 44 |
+
Run in HTTP mode:
|
| 45 |
+
|
| 46 |
+
```bash
|
| 47 |
+
MCP_TRANSPORT=http MCP_PORT=7860 python dateutil/mcp_output/start_mcp.py
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Then connect MCP clients to:
|
| 51 |
+
|
| 52 |
+
- `http://localhost:7860/mcp`
|
| 53 |
+
|
| 54 |
+
For HuggingFace Spaces, the public endpoint is:
|
| 55 |
+
|
| 56 |
+
- `https://<your-space-host>/mcp`
|
| 57 |
+
|
| 58 |
+
## Docker
|
| 59 |
+
|
| 60 |
+
Build and run:
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
bash run_docker.sh
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
Or on PowerShell:
|
| 67 |
+
|
| 68 |
+
```powershell
|
| 69 |
+
./run_docker.ps1
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
The Docker entrypoint is `python dateutil/mcp_output/start_mcp.py`.
|
app.py
CHANGED
|
@@ -1,45 +1,63 @@
|
|
| 1 |
-
from
|
|
|
|
| 2 |
import os
|
| 3 |
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
sys.path.insert(0, mcp_plugin_path)
|
| 7 |
|
| 8 |
-
app = FastAPI(
|
| 9 |
-
title="Dateutil MCP Service",
|
| 10 |
-
description="Auto-generated MCP service for dateutil",
|
| 11 |
-
version="1.0.0"
|
| 12 |
-
)
|
| 13 |
|
| 14 |
@app.get("/")
|
| 15 |
-
def
|
| 16 |
return {
|
| 17 |
-
"service": "
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
|
|
|
| 21 |
}
|
| 22 |
|
|
|
|
| 23 |
@app.get("/health")
|
| 24 |
-
def
|
| 25 |
-
return {"status": "healthy"
|
|
|
|
| 26 |
|
| 27 |
@app.get("/tools")
|
| 28 |
-
def
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
import os
|
| 4 |
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from fastapi import FastAPI # type: ignore[import-not-found]
|
| 8 |
+
|
| 9 |
+
ROOT_DIR = Path(__file__).resolve().parent
|
| 10 |
+
PLUGIN_DIR = (ROOT_DIR / "dateutil" / "mcp_output" / "mcp_plugin").resolve()
|
| 11 |
+
if str(PLUGIN_DIR) not in sys.path:
|
| 12 |
+
sys.path.insert(0, str(PLUGIN_DIR))
|
| 13 |
|
| 14 |
+
app = FastAPI(title="dateutil MCP info app", version="1.0.0")
|
|
|
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
@app.get("/")
|
| 18 |
+
def service_info() -> dict:
|
| 19 |
return {
|
| 20 |
+
"service": "dateutil-mcp",
|
| 21 |
+
"description": "Supplementary info API for dateutil MCP deployment",
|
| 22 |
+
"mcp_entrypoint": "dateutil/mcp_output/start_mcp.py",
|
| 23 |
+
"transport_default": "stdio",
|
| 24 |
+
"http_endpoint": "/mcp when MCP_TRANSPORT=http",
|
| 25 |
}
|
| 26 |
|
| 27 |
+
|
| 28 |
@app.get("/health")
|
| 29 |
+
def health() -> dict:
|
| 30 |
+
return {"status": "healthy"}
|
| 31 |
+
|
| 32 |
|
| 33 |
@app.get("/tools")
|
| 34 |
+
def tools() -> dict:
|
| 35 |
+
import importlib
|
| 36 |
+
|
| 37 |
+
service_module = importlib.import_module("mcp_service")
|
| 38 |
+
create_app = getattr(service_module, "create_app")
|
| 39 |
+
mcp = create_app()
|
| 40 |
+
raw_tools = getattr(mcp, "tools", None)
|
| 41 |
+
|
| 42 |
+
items: list[dict] = []
|
| 43 |
+
if isinstance(raw_tools, dict):
|
| 44 |
+
for key, tool_obj in raw_tools.items():
|
| 45 |
+
items.append(
|
| 46 |
+
{
|
| 47 |
+
"name": getattr(tool_obj, "name", str(key)),
|
| 48 |
+
"description": getattr(tool_obj, "description", ""),
|
| 49 |
+
}
|
| 50 |
+
)
|
| 51 |
+
elif isinstance(raw_tools, list):
|
| 52 |
+
for tool_obj in raw_tools:
|
| 53 |
+
items.append(
|
| 54 |
+
{
|
| 55 |
+
"name": getattr(tool_obj, "name", "unknown"),
|
| 56 |
+
"description": getattr(tool_obj, "description", ""),
|
| 57 |
+
}
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
return {"count": len(items), "tools": items}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
PORT = int(os.getenv("PORT", "7860"))
|
dateutil/mcp_output/README_MCP.md
CHANGED
|
@@ -1,129 +1,89 @@
|
|
| 1 |
-
# dateutil MCP
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
-
|
| 9 |
-
-
|
| 10 |
-
-
|
| 11 |
-
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
--
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
###
|
| 23 |
-
-
|
| 24 |
-
- `
|
| 25 |
-
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
-
|
| 39 |
-
-
|
| 40 |
-
-
|
| 41 |
-
|
| 42 |
-
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
-
|
| 51 |
-
|
| 52 |
-
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
-
|
| 57 |
-
|
| 58 |
-
-
|
| 59 |
-
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
6. `calendar.easter`
|
| 91 |
-
- Purpose: Compute Easter date by year
|
| 92 |
-
- Core module: `dateutil.easter.easter`
|
| 93 |
-
|
| 94 |
-
7. `datetime.utils`
|
| 95 |
-
- Purpose: Small helpers (`today`, `default_tzinfo`, `within_delta`)
|
| 96 |
-
- Core module: `dateutil.utils`
|
| 97 |
-
|
| 98 |
-
---
|
| 99 |
-
|
| 100 |
-
## 5) Common Issues and Notes
|
| 101 |
-
|
| 102 |
-
- Ambiguous or non-existent local times:
|
| 103 |
-
- DST transitions can produce duplicate or invalid local timestamps.
|
| 104 |
-
- Use `datetime_ambiguous`, `datetime_exists`, and `resolve_imaginary`.
|
| 105 |
-
|
| 106 |
-
- Parsing ambiguity:
|
| 107 |
-
- Free-form parsing may infer unintended day/month ordering.
|
| 108 |
-
- Prefer strict ISO endpoint for critical pipelines.
|
| 109 |
-
|
| 110 |
-
- Time zone data differences:
|
| 111 |
-
- Platform behavior may vary (especially Windows-specific timezone handling via `tzwin`).
|
| 112 |
-
|
| 113 |
-
- Performance:
|
| 114 |
-
- Large recurrence expansions can be expensive.
|
| 115 |
-
- Require range limits (`start`, `end`, `count`) in endpoint contracts.
|
| 116 |
-
|
| 117 |
-
- Testing/dev dependencies:
|
| 118 |
-
- `pytest`, `hypothesis`, and Sphinx tooling are for tests/docs, not required at runtime.
|
| 119 |
-
|
| 120 |
-
---
|
| 121 |
-
|
| 122 |
-
## 6) Reference Links / Documentation
|
| 123 |
-
|
| 124 |
-
- Upstream repository: https://github.com/dateutil/dateutil
|
| 125 |
-
- Official docs: https://dateutil.readthedocs.io/
|
| 126 |
-
- PyPI package: https://pypi.org/project/python-dateutil/
|
| 127 |
-
- RFC 5545 recurrence background: https://datatracker.ietf.org/doc/html/rfc5545
|
| 128 |
-
|
| 129 |
-
If needed, I can also generate a production-ready `mcp_server.py` endpoint skeleton aligned to the tool list above.
|
|
|
|
| 1 |
+
# dateutil MCP Plugin
|
| 2 |
+
|
| 3 |
+
This MCP layer exposes key capabilities from `python-dateutil` for parsing, recurrence handling, timezone resolution, and calendar calculations.
|
| 4 |
+
|
| 5 |
+
## Exposed Tools
|
| 6 |
+
|
| 7 |
+
### 1) `health_check`
|
| 8 |
+
- Parameters: none
|
| 9 |
+
- Returns dependency health and adapter load state.
|
| 10 |
+
- Example:
|
| 11 |
+
- Call: `health_check()`
|
| 12 |
+
|
| 13 |
+
### 2) `parse_datetime`
|
| 14 |
+
- Parameters:
|
| 15 |
+
- `value: str`
|
| 16 |
+
- `dayfirst: bool = False`
|
| 17 |
+
- `yearfirst: bool = False`
|
| 18 |
+
- Parses free-form datetime strings.
|
| 19 |
+
- Example:
|
| 20 |
+
- Call: `parse_datetime(value="2026-03-23 10:20", dayfirst=False, yearfirst=True)`
|
| 21 |
+
|
| 22 |
+
### 3) `parse_iso_datetime`
|
| 23 |
+
- Parameters:
|
| 24 |
+
- `value: str`
|
| 25 |
+
- Parses strict ISO-8601 datetime text.
|
| 26 |
+
- Example:
|
| 27 |
+
- Call: `parse_iso_datetime(value="2026-03-23T10:20:00+08:00")`
|
| 28 |
+
|
| 29 |
+
### 4) `compute_relativedelta`
|
| 30 |
+
- Parameters:
|
| 31 |
+
- `start: str`
|
| 32 |
+
- `end: str`
|
| 33 |
+
- Computes calendar-aware delta between two date/time values.
|
| 34 |
+
- Example:
|
| 35 |
+
- Call: `compute_relativedelta(start="2025-01-01", end="2026-03-23")`
|
| 36 |
+
|
| 37 |
+
### 5) `generate_rrule_occurrences`
|
| 38 |
+
- Parameters:
|
| 39 |
+
- `frequency: str` (`YEARLY|MONTHLY|WEEKLY|DAILY`)
|
| 40 |
+
- `dtstart: str`
|
| 41 |
+
- `count: int = 5`
|
| 42 |
+
- `interval: int = 1`
|
| 43 |
+
- Generates recurrence instances using `dateutil.rrule`.
|
| 44 |
+
- Example:
|
| 45 |
+
- Call: `generate_rrule_occurrences(frequency="WEEKLY", dtstart="2026-01-01", count=4, interval=1)`
|
| 46 |
+
|
| 47 |
+
### 6) `resolve_timezone`
|
| 48 |
+
- Parameters:
|
| 49 |
+
- `tz_name: str`
|
| 50 |
+
- Resolves IANA timezone and reports current offset.
|
| 51 |
+
- Example:
|
| 52 |
+
- Call: `resolve_timezone(tz_name="Asia/Shanghai")`
|
| 53 |
+
|
| 54 |
+
### 7) `calculate_easter`
|
| 55 |
+
- Parameters:
|
| 56 |
+
- `year: int`
|
| 57 |
+
- Returns Easter Sunday date for a given year.
|
| 58 |
+
- Example:
|
| 59 |
+
- Call: `calculate_easter(year=2027)`
|
| 60 |
+
|
| 61 |
+
### 8) `list_loaded_modules`
|
| 62 |
+
- Parameters: none
|
| 63 |
+
- Lists adapter-loaded and failed modules.
|
| 64 |
+
- Example:
|
| 65 |
+
- Call: `list_loaded_modules()`
|
| 66 |
+
|
| 67 |
+
## Run Locally (stdio)
|
| 68 |
+
|
| 69 |
+
From `dateutil/mcp_output/`:
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
python start_mcp.py
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Or explicitly:
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
MCP_TRANSPORT=stdio MCP_PORT=8000 python start_mcp.py
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
## Run via HTTP Transport
|
| 82 |
+
|
| 83 |
+
```bash
|
| 84 |
+
MCP_TRANSPORT=http MCP_PORT=7860 python start_mcp.py
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
The MCP endpoint is served by FastMCP at:
|
| 88 |
+
|
| 89 |
+
- `http://localhost:7860/mcp`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dateutil/mcp_output/mcp_plugin/adapter.py
CHANGED
|
@@ -1,432 +1,208 @@
|
|
| 1 |
-
import
|
| 2 |
-
|
| 3 |
import importlib
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
)
|
| 10 |
-
if source_path not in sys.path:
|
| 11 |
-
sys.path.insert(0, source_path)
|
| 12 |
|
| 13 |
|
| 14 |
class Adapter:
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
self._modules: Dict[str, Any] = {}
|
| 25 |
-
self._import_errors: Dict[str, str] = {}
|
| 26 |
-
self._initialize_imports()
|
| 27 |
-
|
| 28 |
-
# -------------------------------------------------------------------------
|
| 29 |
-
# Internal helpers
|
| 30 |
-
# -------------------------------------------------------------------------
|
| 31 |
-
def _result(
|
| 32 |
-
self,
|
| 33 |
-
status: str,
|
| 34 |
-
data: Optional[Any] = None,
|
| 35 |
-
message: str = "",
|
| 36 |
-
error: str = "",
|
| 37 |
-
meta: Optional[Dict[str, Any]] = None,
|
| 38 |
-
) -> Dict[str, Any]:
|
| 39 |
-
payload = {"status": status}
|
| 40 |
-
if message:
|
| 41 |
-
payload["message"] = message
|
| 42 |
-
if error:
|
| 43 |
-
payload["error"] = error
|
| 44 |
-
if data is not None:
|
| 45 |
-
payload["data"] = data
|
| 46 |
-
if meta is not None:
|
| 47 |
-
payload["meta"] = meta
|
| 48 |
-
return payload
|
| 49 |
-
|
| 50 |
-
def _initialize_imports(self) -> None:
|
| 51 |
-
module_names = [
|
| 52 |
-
"src.dateutil",
|
| 53 |
-
"src.dateutil.easter",
|
| 54 |
-
"src.dateutil.parser",
|
| 55 |
-
"src.dateutil.parser._parser",
|
| 56 |
-
"src.dateutil.parser.isoparser",
|
| 57 |
-
"src.dateutil.relativedelta",
|
| 58 |
-
"src.dateutil.rrule",
|
| 59 |
-
"src.dateutil.tz",
|
| 60 |
-
"src.dateutil.tz.tz",
|
| 61 |
-
"src.dateutil.tz.win",
|
| 62 |
-
"src.dateutil.tzwin",
|
| 63 |
-
"src.dateutil.utils",
|
| 64 |
-
"src.dateutil.zoneinfo",
|
| 65 |
-
"src.dateutil.zoneinfo.rebuild",
|
| 66 |
]
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
try:
|
| 69 |
-
|
|
|
|
| 70 |
except Exception as exc:
|
| 71 |
-
self.
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
"
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
)
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
return
|
| 201 |
-
"error",
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
if
|
| 218 |
-
return
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
"""
|
| 243 |
-
mod_res = self._get_module("src.dateutil.relativedelta")
|
| 244 |
-
if mod_res["status"] != "success":
|
| 245 |
-
return mod_res
|
| 246 |
-
try:
|
| 247 |
-
cls = getattr(mod_res["data"], "relativedelta")
|
| 248 |
-
return self._result("success", data=cls(*args, **kwargs))
|
| 249 |
-
except Exception as exc:
|
| 250 |
-
return self._result(
|
| 251 |
-
"error",
|
| 252 |
-
error=f"Failed to create relativedelta: {exc}",
|
| 253 |
-
message="Check relativedelta arguments.",
|
| 254 |
-
)
|
| 255 |
-
|
| 256 |
-
# -------------------------------------------------------------------------
|
| 257 |
-
# dateutil.rrule
|
| 258 |
-
# -------------------------------------------------------------------------
|
| 259 |
-
def create_rrule(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 260 |
-
mod_res = self._get_module("src.dateutil.rrule")
|
| 261 |
-
if mod_res["status"] != "success":
|
| 262 |
-
return mod_res
|
| 263 |
-
try:
|
| 264 |
-
cls = getattr(mod_res["data"], "rrule")
|
| 265 |
-
return self._result("success", data=cls(*args, **kwargs))
|
| 266 |
-
except Exception as exc:
|
| 267 |
-
return self._result("error", error=f"Failed to create rrule: {exc}")
|
| 268 |
-
|
| 269 |
-
def create_rruleset(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
| 270 |
-
mod_res = self._get_module("src.dateutil.rrule")
|
| 271 |
-
if mod_res["status"] != "success":
|
| 272 |
-
return mod_res
|
| 273 |
-
try:
|
| 274 |
-
cls = getattr(mod_res["data"], "rruleset")
|
| 275 |
-
return self._result("success", data=cls(*args, **kwargs))
|
| 276 |
-
except Exception as exc:
|
| 277 |
-
return self._result("error", error=f"Failed to create rruleset: {exc}")
|
| 278 |
-
|
| 279 |
-
def call_rrulestr(self, s: str, **kwargs: Any) -> Dict[str, Any]:
|
| 280 |
-
mod_res = self._get_module("src.dateutil.rrule")
|
| 281 |
-
if mod_res["status"] != "success":
|
| 282 |
-
return mod_res
|
| 283 |
-
try:
|
| 284 |
-
fn = getattr(mod_res["data"], "rrulestr")
|
| 285 |
-
return self._result("success", data=fn(s, **kwargs))
|
| 286 |
-
except Exception as exc:
|
| 287 |
-
return self._result("error", error=f"Failed to parse RRULE string: {exc}")
|
| 288 |
-
|
| 289 |
-
# -------------------------------------------------------------------------
|
| 290 |
-
# dateutil.tz
|
| 291 |
-
# -------------------------------------------------------------------------
|
| 292 |
-
def call_gettz(self, name: Optional[str] = None) -> Dict[str, Any]:
|
| 293 |
-
mod_res = self._get_module("src.dateutil.tz")
|
| 294 |
-
if mod_res["status"] != "success":
|
| 295 |
-
return mod_res
|
| 296 |
-
try:
|
| 297 |
-
fn = getattr(mod_res["data"], "gettz")
|
| 298 |
-
return self._result("success", data=fn(name))
|
| 299 |
-
except Exception as exc:
|
| 300 |
-
return self._result("error", error=f"Failed to resolve timezone: {exc}")
|
| 301 |
-
|
| 302 |
-
def call_datetime_exists(self, dt: Any, tz: Optional[Any] = None) -> Dict[str, Any]:
|
| 303 |
-
mod_res = self._get_module("src.dateutil.tz")
|
| 304 |
-
if mod_res["status"] != "success":
|
| 305 |
-
return mod_res
|
| 306 |
-
try:
|
| 307 |
-
fn = getattr(mod_res["data"], "datetime_exists")
|
| 308 |
-
return self._result("success", data=fn(dt, tz))
|
| 309 |
-
except Exception as exc:
|
| 310 |
-
return self._result("error", error=f"Failed to check datetime existence: {exc}")
|
| 311 |
-
|
| 312 |
-
def call_datetime_ambiguous(self, dt: Any, tz: Optional[Any] = None) -> Dict[str, Any]:
|
| 313 |
-
mod_res = self._get_module("src.dateutil.tz")
|
| 314 |
-
if mod_res["status"] != "success":
|
| 315 |
-
return mod_res
|
| 316 |
-
try:
|
| 317 |
-
fn = getattr(mod_res["data"], "datetime_ambiguous")
|
| 318 |
-
return self._result("success", data=fn(dt, tz))
|
| 319 |
-
except Exception as exc:
|
| 320 |
-
return self._result("error", error=f"Failed to check datetime ambiguity: {exc}")
|
| 321 |
-
|
| 322 |
-
def call_resolve_imaginary(self, dt: Any) -> Dict[str, Any]:
|
| 323 |
-
mod_res = self._get_module("src.dateutil.tz")
|
| 324 |
-
if mod_res["status"] != "success":
|
| 325 |
-
return mod_res
|
| 326 |
-
try:
|
| 327 |
-
fn = getattr(mod_res["data"], "resolve_imaginary")
|
| 328 |
-
return self._result("success", data=fn(dt))
|
| 329 |
-
except Exception as exc:
|
| 330 |
-
return self._result("error", error=f"Failed to resolve imaginary time: {exc}")
|
| 331 |
-
|
| 332 |
-
# -------------------------------------------------------------------------
|
| 333 |
-
# dateutil.utils
|
| 334 |
-
# -------------------------------------------------------------------------
|
| 335 |
-
def call_today(self, tzinfo: Optional[Any] = None) -> Dict[str, Any]:
|
| 336 |
-
mod_res = self._get_module("src.dateutil.utils")
|
| 337 |
-
if mod_res["status"] != "success":
|
| 338 |
-
return mod_res
|
| 339 |
-
try:
|
| 340 |
-
fn = getattr(mod_res["data"], "today")
|
| 341 |
-
return self._result("success", data=fn(tzinfo))
|
| 342 |
-
except Exception as exc:
|
| 343 |
-
return self._result("error", error=f"Failed to compute today(): {exc}")
|
| 344 |
-
|
| 345 |
-
def call_within_delta(self, dt1: Any, dt2: Any, delta: Any) -> Dict[str, Any]:
|
| 346 |
-
mod_res = self._get_module("src.dateutil.utils")
|
| 347 |
-
if mod_res["status"] != "success":
|
| 348 |
-
return mod_res
|
| 349 |
-
try:
|
| 350 |
-
fn = getattr(mod_res["data"], "within_delta")
|
| 351 |
-
return self._result("success", data=fn(dt1, dt2, delta))
|
| 352 |
-
except Exception as exc:
|
| 353 |
-
return self._result("error", error=f"Failed to evaluate within_delta: {exc}")
|
| 354 |
-
|
| 355 |
-
# -------------------------------------------------------------------------
|
| 356 |
-
# dateutil.zoneinfo
|
| 357 |
-
# -------------------------------------------------------------------------
|
| 358 |
-
def call_get_zonefile_instance(self) -> Dict[str, Any]:
|
| 359 |
-
mod_res = self._get_module("src.dateutil.zoneinfo")
|
| 360 |
-
if mod_res["status"] != "success":
|
| 361 |
-
return mod_res
|
| 362 |
-
try:
|
| 363 |
-
fn = getattr(mod_res["data"], "get_zonefile_instance")
|
| 364 |
-
return self._result("success", data=fn())
|
| 365 |
-
except Exception as exc:
|
| 366 |
-
return self._result("error", error=f"Failed to get zonefile instance: {exc}")
|
| 367 |
-
|
| 368 |
-
def call_gettz_zoneinfo(self, name: str) -> Dict[str, Any]:
|
| 369 |
-
mod_res = self._get_module("src.dateutil.zoneinfo")
|
| 370 |
-
if mod_res["status"] != "success":
|
| 371 |
-
return mod_res
|
| 372 |
-
try:
|
| 373 |
-
fn = getattr(mod_res["data"], "gettz")
|
| 374 |
-
return self._result("success", data=fn(name))
|
| 375 |
-
except Exception as exc:
|
| 376 |
-
return self._result("error", error=f"Failed to load zoneinfo timezone: {exc}")
|
| 377 |
-
|
| 378 |
-
def call_rebuild_zoneinfo(self, filename: Optional[str] = None) -> Dict[str, Any]:
|
| 379 |
-
mod_res = self._get_module("src.dateutil.zoneinfo.rebuild")
|
| 380 |
-
if mod_res["status"] != "success":
|
| 381 |
-
return mod_res
|
| 382 |
-
try:
|
| 383 |
-
fn = getattr(mod_res["data"], "rebuild")
|
| 384 |
-
if filename is None:
|
| 385 |
-
return self._result("success", data=fn())
|
| 386 |
-
return self._result("success", data=fn(filename))
|
| 387 |
-
except Exception as exc:
|
| 388 |
-
return self._result(
|
| 389 |
-
"error",
|
| 390 |
-
error=f"Failed to rebuild zoneinfo data: {exc}",
|
| 391 |
-
message="Provide a valid zoneinfo tarball path or run with default configuration.",
|
| 392 |
-
)
|
| 393 |
-
|
| 394 |
-
# -------------------------------------------------------------------------
|
| 395 |
-
# Discovery helpers
|
| 396 |
-
# -------------------------------------------------------------------------
|
| 397 |
-
def list_available_modules(self) -> Dict[str, Any]:
|
| 398 |
-
"""
|
| 399 |
-
List imported and failed modules for quick adapter introspection.
|
| 400 |
-
|
| 401 |
-
Returns:
|
| 402 |
-
dict: status with module availability details.
|
| 403 |
-
"""
|
| 404 |
-
return self._result(
|
| 405 |
-
"success",
|
| 406 |
-
data={
|
| 407 |
-
"available": sorted(self._modules.keys()),
|
| 408 |
-
"failed": self._import_errors,
|
| 409 |
-
},
|
| 410 |
-
)
|
| 411 |
-
|
| 412 |
-
def list_module_attributes(self, module_name: str, public_only: bool = True) -> Dict[str, Any]:
|
| 413 |
-
"""
|
| 414 |
-
List attributes for an imported module.
|
| 415 |
-
|
| 416 |
-
Parameters:
|
| 417 |
-
module_name (str): Fully-qualified module name.
|
| 418 |
-
public_only (bool): If True, omit private names starting with underscore.
|
| 419 |
-
|
| 420 |
-
Returns:
|
| 421 |
-
dict: status with attribute names or fallback/error details.
|
| 422 |
-
"""
|
| 423 |
-
mod_res = self._get_module(module_name)
|
| 424 |
-
if mod_res["status"] != "success":
|
| 425 |
-
return mod_res
|
| 426 |
-
try:
|
| 427 |
-
names: List[str] = dir(mod_res["data"])
|
| 428 |
-
if public_only:
|
| 429 |
-
names = [n for n in names if not n.startswith("_")]
|
| 430 |
-
return self._result("success", data=sorted(names))
|
| 431 |
-
except Exception as exc:
|
| 432 |
-
return self._result("error", error=f"Failed to inspect module attributes: {exc}")
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
import importlib
|
| 4 |
+
import inspect
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from types import ModuleType
|
| 8 |
+
from typing import Any
|
| 9 |
|
| 10 |
+
CURRENT_DIR = Path(__file__).resolve().parent
|
| 11 |
+
SOURCE_DIR = (CURRENT_DIR / ".." / ".." / "source").resolve()
|
| 12 |
+
if str(SOURCE_DIR) not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(SOURCE_DIR))
|
|
|
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
class Adapter:
|
| 17 |
+
def __init__(self, module_names: list[str] | None = None) -> None:
|
| 18 |
+
self.module_names = module_names or [
|
| 19 |
+
"dateutil",
|
| 20 |
+
"dateutil.parser",
|
| 21 |
+
"dateutil.relativedelta",
|
| 22 |
+
"dateutil.rrule",
|
| 23 |
+
"dateutil.tz",
|
| 24 |
+
"dateutil.easter",
|
| 25 |
+
"dateutil.zoneinfo",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
]
|
| 27 |
+
self.loaded_modules: dict[str, ModuleType] = {}
|
| 28 |
+
self.failed_modules: dict[str, str] = {}
|
| 29 |
+
self.mode = "blackbox"
|
| 30 |
+
self._load_modules()
|
| 31 |
+
|
| 32 |
+
def _load_modules(self) -> None:
|
| 33 |
+
for module_name in self.module_names:
|
| 34 |
try:
|
| 35 |
+
module = importlib.import_module(module_name)
|
| 36 |
+
self.loaded_modules[module_name] = module
|
| 37 |
except Exception as exc:
|
| 38 |
+
self.failed_modules[module_name] = str(exc)
|
| 39 |
+
|
| 40 |
+
self.mode = "active" if self.loaded_modules else "blackbox"
|
| 41 |
+
|
| 42 |
+
def health(self) -> dict[str, Any]:
|
| 43 |
+
status = "ok" if self.loaded_modules else "fallback"
|
| 44 |
+
return {
|
| 45 |
+
"status": status,
|
| 46 |
+
"mode": self.mode,
|
| 47 |
+
"loaded_count": len(self.loaded_modules),
|
| 48 |
+
"failed_count": len(self.failed_modules),
|
| 49 |
+
"loaded_modules": sorted(self.loaded_modules.keys()),
|
| 50 |
+
"failed_modules": self.failed_modules,
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def list_modules(self) -> dict[str, Any]:
|
| 54 |
+
status = "ok" if self.loaded_modules else "fallback"
|
| 55 |
+
return {
|
| 56 |
+
"status": status,
|
| 57 |
+
"mode": self.mode,
|
| 58 |
+
"loaded": sorted(self.loaded_modules.keys()),
|
| 59 |
+
"failed": self.failed_modules,
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
def list_symbols(self, module_name: str, public_only: bool = True) -> dict[str, Any]:
|
| 63 |
+
module = self.loaded_modules.get(module_name)
|
| 64 |
+
if module is None:
|
| 65 |
+
if module_name in self.failed_modules:
|
| 66 |
+
return {
|
| 67 |
+
"status": "error",
|
| 68 |
+
"module": module_name,
|
| 69 |
+
"error": self.failed_modules[module_name],
|
| 70 |
+
}
|
| 71 |
+
return {
|
| 72 |
+
"status": "error",
|
| 73 |
+
"module": module_name,
|
| 74 |
+
"error": "Module not loaded",
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
try:
|
| 78 |
+
symbols = []
|
| 79 |
+
for name in dir(module):
|
| 80 |
+
if public_only and name.startswith("_"):
|
| 81 |
+
continue
|
| 82 |
+
value = getattr(module, name)
|
| 83 |
+
if inspect.ismodule(value):
|
| 84 |
+
kind = "module"
|
| 85 |
+
elif inspect.isclass(value):
|
| 86 |
+
kind = "class"
|
| 87 |
+
elif inspect.isfunction(value) or inspect.isbuiltin(value):
|
| 88 |
+
kind = "function"
|
| 89 |
+
else:
|
| 90 |
+
kind = type(value).__name__
|
| 91 |
+
symbols.append({"name": name, "kind": kind})
|
| 92 |
+
|
| 93 |
+
return {
|
| 94 |
+
"status": "ok",
|
| 95 |
+
"module": module_name,
|
| 96 |
+
"count": len(symbols),
|
| 97 |
+
"symbols": symbols,
|
| 98 |
+
}
|
| 99 |
+
except Exception as exc:
|
| 100 |
+
return {
|
| 101 |
+
"status": "error",
|
| 102 |
+
"module": module_name,
|
| 103 |
+
"error": str(exc),
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
def call_function(
|
| 107 |
+
self,
|
| 108 |
+
module_name: str,
|
| 109 |
+
function_name: str,
|
| 110 |
+
args: list[Any] | None = None,
|
| 111 |
+
kwargs: dict[str, Any] | None = None,
|
| 112 |
+
) -> dict[str, Any]:
|
| 113 |
+
module = self.loaded_modules.get(module_name)
|
| 114 |
+
if module is None:
|
| 115 |
+
return {
|
| 116 |
+
"status": "error",
|
| 117 |
+
"module": module_name,
|
| 118 |
+
"function": function_name,
|
| 119 |
+
"error": "Module not loaded",
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
function_obj = getattr(module, function_name)
|
| 124 |
+
except AttributeError:
|
| 125 |
+
return {
|
| 126 |
+
"status": "error",
|
| 127 |
+
"module": module_name,
|
| 128 |
+
"function": function_name,
|
| 129 |
+
"error": "Function not found",
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
if not callable(function_obj):
|
| 133 |
+
return {
|
| 134 |
+
"status": "error",
|
| 135 |
+
"module": module_name,
|
| 136 |
+
"function": function_name,
|
| 137 |
+
"error": "Target is not callable",
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
try:
|
| 141 |
+
final_args = args or []
|
| 142 |
+
final_kwargs = kwargs or {}
|
| 143 |
+
result = function_obj(*final_args, **final_kwargs)
|
| 144 |
+
return {
|
| 145 |
+
"status": "ok",
|
| 146 |
+
"module": module_name,
|
| 147 |
+
"function": function_name,
|
| 148 |
+
"result": result,
|
| 149 |
+
}
|
| 150 |
+
except Exception as exc:
|
| 151 |
+
return {
|
| 152 |
+
"status": "error",
|
| 153 |
+
"module": module_name,
|
| 154 |
+
"function": function_name,
|
| 155 |
+
"error": str(exc),
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
def create_instance(
|
| 159 |
+
self,
|
| 160 |
+
module_name: str,
|
| 161 |
+
class_name: str,
|
| 162 |
+
args: list[Any] | None = None,
|
| 163 |
+
kwargs: dict[str, Any] | None = None,
|
| 164 |
+
) -> dict[str, Any]:
|
| 165 |
+
module = self.loaded_modules.get(module_name)
|
| 166 |
+
if module is None:
|
| 167 |
+
return {
|
| 168 |
+
"status": "error",
|
| 169 |
+
"module": module_name,
|
| 170 |
+
"class": class_name,
|
| 171 |
+
"error": "Module not loaded",
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
class_obj = getattr(module, class_name)
|
| 176 |
+
except AttributeError:
|
| 177 |
+
return {
|
| 178 |
+
"status": "error",
|
| 179 |
+
"module": module_name,
|
| 180 |
+
"class": class_name,
|
| 181 |
+
"error": "Class not found",
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
if not inspect.isclass(class_obj):
|
| 185 |
+
return {
|
| 186 |
+
"status": "error",
|
| 187 |
+
"module": module_name,
|
| 188 |
+
"class": class_name,
|
| 189 |
+
"error": "Target is not a class",
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
try:
|
| 193 |
+
final_args = args or []
|
| 194 |
+
final_kwargs = kwargs or {}
|
| 195 |
+
instance = class_obj(*final_args, **final_kwargs)
|
| 196 |
+
return {
|
| 197 |
+
"status": "ok",
|
| 198 |
+
"module": module_name,
|
| 199 |
+
"class": class_name,
|
| 200 |
+
"instance_type": type(instance).__name__,
|
| 201 |
+
}
|
| 202 |
+
except Exception as exc:
|
| 203 |
+
return {
|
| 204 |
+
"status": "error",
|
| 205 |
+
"module": module_name,
|
| 206 |
+
"class": class_name,
|
| 207 |
+
"error": str(exc),
|
| 208 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dateutil/mcp_output/mcp_plugin/main.py
CHANGED
|
@@ -1,13 +1,6 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MCP Service Auto-Wrapper - Auto-generated
|
| 3 |
-
"""
|
| 4 |
from mcp_service import create_app
|
| 5 |
|
| 6 |
-
|
| 7 |
-
"""Main entry point"""
|
| 8 |
-
app = create_app()
|
| 9 |
-
return app
|
| 10 |
-
|
| 11 |
if __name__ == "__main__":
|
| 12 |
-
app =
|
| 13 |
-
app.run()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from mcp_service import create_app
|
| 2 |
|
| 3 |
+
# Local stdio entry point only (Claude Desktop / CLI), not for web or Docker deployment.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
if __name__ == "__main__":
|
| 5 |
+
app = create_app()
|
| 6 |
+
app.run()
|
dateutil/mcp_output/mcp_plugin/mcp_service.py
CHANGED
|
@@ -1,242 +1,278 @@
|
|
| 1 |
-
import
|
| 2 |
-
import sys
|
| 3 |
-
from datetime import datetime, date
|
| 4 |
-
from typing import List, Optional, Dict, Any
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
if source_path not in sys.path:
|
| 11 |
-
sys.path.insert(0, source_path)
|
| 12 |
|
| 13 |
from fastmcp import FastMCP
|
| 14 |
-
from dateutil import easter
|
| 15 |
-
from dateutil.parser import parse as dt_parse
|
| 16 |
-
from dateutil.parser import isoparse
|
| 17 |
-
from dateutil.relativedelta import relativedelta
|
| 18 |
-
from dateutil.rrule import rrule, rrulestr, YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY
|
| 19 |
-
from dateutil.tz import gettz, UTC
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
mcp = FastMCP("dateutil_service")
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
_FREQ_MAP = {
|
| 26 |
-
"YEARLY": YEARLY,
|
| 27 |
-
"MONTHLY": MONTHLY,
|
| 28 |
-
"WEEKLY": WEEKLY,
|
| 29 |
-
"DAILY": DAILY,
|
| 30 |
-
"HOURLY": HOURLY,
|
| 31 |
-
"MINUTELY": MINUTELY,
|
| 32 |
-
"SECONDLY": SECONDLY,
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def _safe_result(value: Any) -> Dict[str, Any]:
|
| 37 |
-
return {"success": True, "result": value, "error": None}
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
"""
|
| 52 |
-
Parse a
|
| 53 |
|
| 54 |
Parameters:
|
| 55 |
-
-
|
| 56 |
-
- dayfirst:
|
| 57 |
-
- yearfirst:
|
| 58 |
-
- default_iso: Optional ISO datetime used as default values for missing parts.
|
| 59 |
-
|
| 60 |
-
Returns:
|
| 61 |
-
- Dictionary with success/result/error.
|
| 62 |
"""
|
|
|
|
|
|
|
|
|
|
| 63 |
try:
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
default_dt = datetime.fromisoformat(default_iso)
|
| 67 |
-
parsed = dt_parse(text, dayfirst=dayfirst, yearfirst=yearfirst, default=default_dt)
|
| 68 |
-
return _safe_result(parsed.isoformat())
|
| 69 |
except Exception as exc:
|
| 70 |
-
return
|
| 71 |
|
| 72 |
|
| 73 |
-
@mcp.tool(name="parse_iso_datetime", description="Parse
|
| 74 |
-
def parse_iso_datetime(
|
| 75 |
"""
|
| 76 |
-
Parse an ISO-8601 datetime string
|
| 77 |
|
| 78 |
Parameters:
|
| 79 |
-
-
|
| 80 |
-
|
| 81 |
-
Returns:
|
| 82 |
-
- Dictionary with success/result/error.
|
| 83 |
"""
|
|
|
|
|
|
|
|
|
|
| 84 |
try:
|
| 85 |
-
parsed =
|
| 86 |
-
return
|
| 87 |
except Exception as exc:
|
| 88 |
-
return
|
| 89 |
|
| 90 |
|
| 91 |
-
@mcp.tool(name="
|
| 92 |
-
def
|
| 93 |
"""
|
| 94 |
-
Compute
|
| 95 |
|
| 96 |
Parameters:
|
| 97 |
-
-
|
| 98 |
-
-
|
| 99 |
-
|
| 100 |
-
Returns:
|
| 101 |
-
- Dictionary with success/result/error.
|
| 102 |
"""
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
return _safe_result(easter_date.isoformat())
|
| 106 |
-
except Exception as exc:
|
| 107 |
-
return _safe_error(exc)
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
@mcp.tool(name="add_relativedelta", description="Apply relative date/time offsets to a datetime.")
|
| 111 |
-
def add_relativedelta(
|
| 112 |
-
base_iso: str,
|
| 113 |
-
years: int = 0,
|
| 114 |
-
months: int = 0,
|
| 115 |
-
weeks: int = 0,
|
| 116 |
-
days: int = 0,
|
| 117 |
-
hours: int = 0,
|
| 118 |
-
minutes: int = 0,
|
| 119 |
-
seconds: int = 0
|
| 120 |
-
) -> Dict[str, Any]:
|
| 121 |
-
"""
|
| 122 |
-
Apply relativedelta offsets to a base datetime.
|
| 123 |
-
|
| 124 |
-
Parameters:
|
| 125 |
-
- base_iso: Base datetime in ISO format.
|
| 126 |
-
- years, months, weeks, days, hours, minutes, seconds: Relative offsets.
|
| 127 |
|
| 128 |
-
Returns:
|
| 129 |
-
- Dictionary with success/result/error.
|
| 130 |
-
"""
|
| 131 |
try:
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
)
|
| 142 |
-
out = base_dt + delta
|
| 143 |
-
return _safe_result(out.isoformat())
|
| 144 |
except Exception as exc:
|
| 145 |
-
return
|
| 146 |
|
| 147 |
|
| 148 |
-
@mcp.tool(name="
|
| 149 |
-
def
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
count: int =
|
| 153 |
-
interval: int = 1
|
| 154 |
-
) ->
|
| 155 |
"""
|
| 156 |
-
Generate
|
| 157 |
|
| 158 |
Parameters:
|
| 159 |
-
-
|
| 160 |
-
-
|
| 161 |
-
- count: Number of occurrences to
|
| 162 |
-
- interval:
|
| 163 |
-
|
| 164 |
-
Returns:
|
| 165 |
-
- Dictionary with success/result/error.
|
| 166 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
try:
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
except Exception as exc:
|
| 176 |
-
return
|
| 177 |
|
| 178 |
|
| 179 |
-
@mcp.tool(name="
|
| 180 |
-
def
|
| 181 |
-
rule_text: str,
|
| 182 |
-
dtstart_iso: Optional[str] = None,
|
| 183 |
-
limit: int = 20
|
| 184 |
-
) -> Dict[str, Any]:
|
| 185 |
"""
|
| 186 |
-
|
| 187 |
|
| 188 |
Parameters:
|
| 189 |
-
-
|
| 190 |
-
- dtstart_iso: Optional ISO datetime start.
|
| 191 |
-
- limit: Maximum number of occurrences returned.
|
| 192 |
-
|
| 193 |
-
Returns:
|
| 194 |
-
- Dictionary with success/result/error.
|
| 195 |
"""
|
|
|
|
|
|
|
|
|
|
| 196 |
try:
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
except Exception as exc:
|
| 206 |
-
return
|
| 207 |
|
| 208 |
|
| 209 |
-
@mcp.tool(name="
|
| 210 |
-
def
|
| 211 |
-
dt_iso: str,
|
| 212 |
-
from_tz_name: str,
|
| 213 |
-
to_tz_name: str
|
| 214 |
-
) -> Dict[str, Any]:
|
| 215 |
"""
|
| 216 |
-
|
| 217 |
|
| 218 |
Parameters:
|
| 219 |
-
-
|
| 220 |
-
- from_tz_name: Source timezone name (e.g., 'UTC', 'America/New_York').
|
| 221 |
-
- to_tz_name: Target timezone name.
|
| 222 |
-
|
| 223 |
-
Returns:
|
| 224 |
-
- Dictionary with success/result/error.
|
| 225 |
"""
|
|
|
|
|
|
|
|
|
|
| 226 |
try:
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
to_tz = UTC if to_tz_name.upper() == "UTC" else gettz(to_tz_name)
|
| 230 |
-
if from_tz is None or to_tz is None:
|
| 231 |
-
raise ValueError("Invalid timezone name.")
|
| 232 |
-
if dt.tzinfo is None:
|
| 233 |
-
dt = dt.replace(tzinfo=from_tz)
|
| 234 |
-
else:
|
| 235 |
-
dt = dt.astimezone(from_tz)
|
| 236 |
-
converted = dt.astimezone(to_tz)
|
| 237 |
-
return _safe_result(converted.isoformat())
|
| 238 |
except Exception as exc:
|
| 239 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
|
| 242 |
def create_app() -> FastMCP:
|
|
@@ -244,4 +280,4 @@ def create_app() -> FastMCP:
|
|
| 244 |
|
| 245 |
|
| 246 |
if __name__ == "__main__":
|
| 247 |
-
mcp.run()
|
|
|
|
| 1 |
+
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
import datetime as dt
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
|
|
|
|
|
|
| 7 |
|
| 8 |
from fastmcp import FastMCP
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
CURRENT_DIR = Path(__file__).resolve().parent
|
| 11 |
+
SOURCE_DIR = (CURRENT_DIR / ".." / ".." / "source").resolve()
|
| 12 |
+
if str(SOURCE_DIR) not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(SOURCE_DIR))
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
import dateutil # type: ignore
|
| 17 |
+
except Exception:
|
| 18 |
+
dateutil = None
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
from dateutil.parser import parse as dateutil_parse # type: ignore
|
| 22 |
+
except Exception:
|
| 23 |
+
dateutil_parse = None
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
from dateutil.parser import isoparse as dateutil_isoparse # type: ignore
|
| 27 |
+
except Exception:
|
| 28 |
+
dateutil_isoparse = None
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
from dateutil.relativedelta import relativedelta # type: ignore
|
| 32 |
+
except Exception:
|
| 33 |
+
relativedelta = None
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule # type: ignore
|
| 37 |
+
except Exception:
|
| 38 |
+
DAILY = None
|
| 39 |
+
MONTHLY = None
|
| 40 |
+
WEEKLY = None
|
| 41 |
+
YEARLY = None
|
| 42 |
+
rrule = None
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
from dateutil.tz import gettz # type: ignore
|
| 46 |
+
except Exception:
|
| 47 |
+
gettz = None
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
from dateutil.easter import easter # type: ignore
|
| 51 |
+
except Exception:
|
| 52 |
+
easter = None
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
from adapter import Adapter
|
| 56 |
+
except Exception:
|
| 57 |
+
Adapter = None
|
| 58 |
+
|
| 59 |
+
mcp = FastMCP("dateutil-mcp-service")
|
| 60 |
+
adapter = Adapter() if Adapter is not None else None
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _ok(result: Any) -> dict[str, Any]:
|
| 64 |
+
return {"success": True, "result": result, "error": None}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _err(message: str) -> dict[str, Any]:
|
| 68 |
+
return {"success": False, "result": None, "error": message}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _parse_datetime_value(value: str) -> dt.datetime:
|
| 72 |
+
if dateutil_parse is None:
|
| 73 |
+
raise RuntimeError("dateutil.parser.parse is unavailable")
|
| 74 |
+
return dateutil_parse(value)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@mcp.tool(name="health_check", description="Report availability of dateutil and MCP dependencies")
|
| 78 |
+
def health_check() -> dict[str, Any]:
|
| 79 |
+
"""Return dependency and adapter health state."""
|
| 80 |
+
deps = {
|
| 81 |
+
"dateutil": dateutil is not None,
|
| 82 |
+
"dateutil_parse": dateutil_parse is not None,
|
| 83 |
+
"dateutil_isoparse": dateutil_isoparse is not None,
|
| 84 |
+
"relativedelta": relativedelta is not None,
|
| 85 |
+
"rrule": rrule is not None,
|
| 86 |
+
"gettz": gettz is not None,
|
| 87 |
+
"easter": easter is not None,
|
| 88 |
+
"adapter": adapter is not None,
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
result = {
|
| 92 |
+
"dependencies": deps,
|
| 93 |
+
"adapter": adapter.health() if adapter is not None else {"status": "fallback", "mode": "blackbox"},
|
| 94 |
+
}
|
| 95 |
+
return _ok(result)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
@mcp.tool(name="parse_datetime", description="Parse free-form datetime text with dateutil.parser")
|
| 99 |
+
def parse_datetime(value: str, dayfirst: bool = False, yearfirst: bool = False) -> dict[str, Any]:
|
| 100 |
"""
|
| 101 |
+
Parse a free-form date/time string.
|
| 102 |
|
| 103 |
Parameters:
|
| 104 |
+
- value: Input datetime string.
|
| 105 |
+
- dayfirst: Interpret ambiguous dates as day-first.
|
| 106 |
+
- yearfirst: Interpret ambiguous dates as year-first.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
"""
|
| 108 |
+
if dateutil_parse is None:
|
| 109 |
+
return _err("dateutil.parser.parse is unavailable")
|
| 110 |
+
|
| 111 |
try:
|
| 112 |
+
parsed = dateutil_parse(value, dayfirst=dayfirst, yearfirst=yearfirst)
|
| 113 |
+
return _ok({"input": value, "iso": parsed.isoformat()})
|
|
|
|
|
|
|
|
|
|
| 114 |
except Exception as exc:
|
| 115 |
+
return _err(str(exc))
|
| 116 |
|
| 117 |
|
| 118 |
+
@mcp.tool(name="parse_iso_datetime", description="Parse ISO-8601 datetime string")
|
| 119 |
+
def parse_iso_datetime(value: str) -> dict[str, Any]:
|
| 120 |
"""
|
| 121 |
+
Parse an ISO-8601 datetime string.
|
| 122 |
|
| 123 |
Parameters:
|
| 124 |
+
- value: ISO-8601 datetime text.
|
|
|
|
|
|
|
|
|
|
| 125 |
"""
|
| 126 |
+
if dateutil_isoparse is None:
|
| 127 |
+
return _err("dateutil.parser.isoparse is unavailable")
|
| 128 |
+
|
| 129 |
try:
|
| 130 |
+
parsed = dateutil_isoparse(value)
|
| 131 |
+
return _ok({"input": value, "iso": parsed.isoformat()})
|
| 132 |
except Exception as exc:
|
| 133 |
+
return _err(str(exc))
|
| 134 |
|
| 135 |
|
| 136 |
+
@mcp.tool(name="compute_relativedelta", description="Compute calendar-aware delta between two datetimes")
|
| 137 |
+
def compute_relativedelta(start: str, end: str) -> dict[str, Any]:
|
| 138 |
"""
|
| 139 |
+
Compute relativedelta between two datetime strings.
|
| 140 |
|
| 141 |
Parameters:
|
| 142 |
+
- start: Start datetime string.
|
| 143 |
+
- end: End datetime string.
|
|
|
|
|
|
|
|
|
|
| 144 |
"""
|
| 145 |
+
if relativedelta is None:
|
| 146 |
+
return _err("dateutil.relativedelta.relativedelta is unavailable")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
|
|
|
|
|
|
|
|
|
| 148 |
try:
|
| 149 |
+
start_dt = _parse_datetime_value(start)
|
| 150 |
+
end_dt = _parse_datetime_value(end)
|
| 151 |
+
delta = relativedelta(end_dt, start_dt)
|
| 152 |
+
return _ok(
|
| 153 |
+
{
|
| 154 |
+
"start": start_dt.isoformat(),
|
| 155 |
+
"end": end_dt.isoformat(),
|
| 156 |
+
"years": delta.years,
|
| 157 |
+
"months": delta.months,
|
| 158 |
+
"days": delta.days,
|
| 159 |
+
"hours": delta.hours,
|
| 160 |
+
"minutes": delta.minutes,
|
| 161 |
+
"seconds": delta.seconds,
|
| 162 |
+
"microseconds": delta.microseconds,
|
| 163 |
+
}
|
| 164 |
)
|
|
|
|
|
|
|
| 165 |
except Exception as exc:
|
| 166 |
+
return _err(str(exc))
|
| 167 |
|
| 168 |
|
| 169 |
+
@mcp.tool(name="generate_rrule_occurrences", description="Generate recurring datetimes from an rrule")
|
| 170 |
+
def generate_rrule_occurrences(
|
| 171 |
+
frequency: str,
|
| 172 |
+
dtstart: str,
|
| 173 |
+
count: int = 5,
|
| 174 |
+
interval: int = 1,
|
| 175 |
+
) -> dict[str, Any]:
|
| 176 |
"""
|
| 177 |
+
Generate recurring occurrences.
|
| 178 |
|
| 179 |
Parameters:
|
| 180 |
+
- frequency: One of YEARLY, MONTHLY, WEEKLY, DAILY.
|
| 181 |
+
- dtstart: Start datetime string.
|
| 182 |
+
- count: Number of occurrences to generate.
|
| 183 |
+
- interval: Interval for recurrence frequency.
|
|
|
|
|
|
|
|
|
|
| 184 |
"""
|
| 185 |
+
if rrule is None:
|
| 186 |
+
return _err("dateutil.rrule.rrule is unavailable")
|
| 187 |
+
|
| 188 |
+
freq_lookup = {
|
| 189 |
+
"YEARLY": YEARLY,
|
| 190 |
+
"MONTHLY": MONTHLY,
|
| 191 |
+
"WEEKLY": WEEKLY,
|
| 192 |
+
"DAILY": DAILY,
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
normalized = frequency.upper().strip()
|
| 196 |
+
if normalized not in freq_lookup or freq_lookup[normalized] is None:
|
| 197 |
+
return _err("Invalid frequency. Use YEARLY, MONTHLY, WEEKLY, or DAILY")
|
| 198 |
+
|
| 199 |
+
if count < 1:
|
| 200 |
+
return _err("count must be >= 1")
|
| 201 |
+
if interval < 1:
|
| 202 |
+
return _err("interval must be >= 1")
|
| 203 |
+
|
| 204 |
try:
|
| 205 |
+
start_dt = _parse_datetime_value(dtstart)
|
| 206 |
+
rule = rrule(freq_lookup[normalized], dtstart=start_dt, count=count, interval=interval)
|
| 207 |
+
occurrences = [item.isoformat() for item in rule]
|
| 208 |
+
return _ok(
|
| 209 |
+
{
|
| 210 |
+
"frequency": normalized,
|
| 211 |
+
"dtstart": start_dt.isoformat(),
|
| 212 |
+
"count": count,
|
| 213 |
+
"interval": interval,
|
| 214 |
+
"occurrences": occurrences,
|
| 215 |
+
}
|
| 216 |
+
)
|
| 217 |
except Exception as exc:
|
| 218 |
+
return _err(str(exc))
|
| 219 |
|
| 220 |
|
| 221 |
+
@mcp.tool(name="resolve_timezone", description="Resolve timezone by IANA name using dateutil.tz.gettz")
|
| 222 |
+
def resolve_timezone(tz_name: str) -> dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
"""
|
| 224 |
+
Resolve timezone information.
|
| 225 |
|
| 226 |
Parameters:
|
| 227 |
+
- tz_name: IANA timezone name, e.g. 'Asia/Shanghai'.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
"""
|
| 229 |
+
if gettz is None:
|
| 230 |
+
return _err("dateutil.tz.gettz is unavailable")
|
| 231 |
+
|
| 232 |
try:
|
| 233 |
+
tzinfo = gettz(tz_name)
|
| 234 |
+
if tzinfo is None:
|
| 235 |
+
return _err(f"Timezone not found: {tz_name}")
|
| 236 |
+
|
| 237 |
+
now_utc = dt.datetime.now(dt.timezone.utc)
|
| 238 |
+
local_time = now_utc.astimezone(tzinfo)
|
| 239 |
+
offset = local_time.utcoffset()
|
| 240 |
+
return _ok(
|
| 241 |
+
{
|
| 242 |
+
"timezone": tz_name,
|
| 243 |
+
"resolved": True,
|
| 244 |
+
"current_time": local_time.isoformat(),
|
| 245 |
+
"utc_offset_seconds": int(offset.total_seconds()) if offset is not None else None,
|
| 246 |
+
}
|
| 247 |
+
)
|
| 248 |
except Exception as exc:
|
| 249 |
+
return _err(str(exc))
|
| 250 |
|
| 251 |
|
| 252 |
+
@mcp.tool(name="calculate_easter", description="Calculate Easter Sunday date for a given year")
|
| 253 |
+
def calculate_easter(year: int) -> dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
"""
|
| 255 |
+
Calculate Easter Sunday date.
|
| 256 |
|
| 257 |
Parameters:
|
| 258 |
+
- year: Gregorian year.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
"""
|
| 260 |
+
if easter is None:
|
| 261 |
+
return _err("dateutil.easter.easter is unavailable")
|
| 262 |
+
|
| 263 |
try:
|
| 264 |
+
result_date = easter(year)
|
| 265 |
+
return _ok({"year": year, "easter": result_date.isoformat()})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
except Exception as exc:
|
| 267 |
+
return _err(str(exc))
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
@mcp.tool(name="list_loaded_modules", description="List adapter module loading status")
|
| 271 |
+
def list_loaded_modules() -> dict[str, Any]:
|
| 272 |
+
"""List loaded/failed modules reported by adapter."""
|
| 273 |
+
if adapter is None:
|
| 274 |
+
return _err("Adapter unavailable")
|
| 275 |
+
return _ok(adapter.list_modules())
|
| 276 |
|
| 277 |
|
| 278 |
def create_app() -> FastMCP:
|
|
|
|
| 280 |
|
| 281 |
|
| 282 |
if __name__ == "__main__":
|
| 283 |
+
mcp.run()
|
dateutil/mcp_output/requirements.txt
CHANGED
|
@@ -1,8 +1,3 @@
|
|
| 1 |
fastmcp
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
pydantic>=2.0.0
|
| 5 |
-
six >= 1.6
|
| 6 |
-
package_dir=
|
| 7 |
-
=src
|
| 8 |
-
packages = find:
|
|
|
|
| 1 |
fastmcp
|
| 2 |
+
python-dateutil
|
| 3 |
+
six
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dateutil/mcp_output/start_mcp.py
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
|
|
| 1 |
|
| 2 |
-
"""
|
| 3 |
-
MCP Service Startup Entry
|
| 4 |
-
"""
|
| 5 |
-
import sys
|
| 6 |
import os
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
if
|
| 11 |
-
sys.path.insert(0,
|
| 12 |
|
| 13 |
from mcp_service import create_app
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
| 17 |
app = create_app()
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
if transport == "http":
|
| 24 |
app.run(transport="http", host="0.0.0.0", port=port)
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
| 28 |
|
| 29 |
if __name__ == "__main__":
|
| 30 |
main()
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import os
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
|
| 7 |
+
CURRENT_DIR = Path(__file__).resolve().parent
|
| 8 |
+
PLUGIN_DIR = (CURRENT_DIR / "mcp_plugin").resolve()
|
| 9 |
+
if str(PLUGIN_DIR) not in sys.path:
|
| 10 |
+
sys.path.insert(0, str(PLUGIN_DIR))
|
| 11 |
|
| 12 |
from mcp_service import create_app
|
| 13 |
|
| 14 |
+
|
| 15 |
+
def main() -> None:
|
| 16 |
+
transport = os.getenv("MCP_TRANSPORT", "stdio").strip().lower()
|
| 17 |
+
port = int(os.getenv("MCP_PORT", "8000"))
|
| 18 |
+
|
| 19 |
app = create_app()
|
| 20 |
+
|
| 21 |
+
if transport == "stdio":
|
| 22 |
+
app.run(transport="stdio")
|
| 23 |
+
return
|
| 24 |
+
|
| 25 |
if transport == "http":
|
| 26 |
app.run(transport="http", host="0.0.0.0", port=port)
|
| 27 |
+
return
|
| 28 |
+
|
| 29 |
+
raise ValueError("Unsupported MCP_TRANSPORT. Use 'stdio' or 'http'.")
|
| 30 |
+
|
| 31 |
|
| 32 |
if __name__ == "__main__":
|
| 33 |
main()
|
port.json
CHANGED
|
@@ -1,5 +1 @@
|
|
| 1 |
-
{
|
| 2 |
-
"repo": "dateutil",
|
| 3 |
-
"port": 7951,
|
| 4 |
-
"timestamp": 1773283013
|
| 5 |
-
}
|
|
|
|
| 1 |
+
{"port": 7860}
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
fastmcp
|
|
|
|
|
|
|
| 2 |
fastapi
|
| 3 |
-
uvicorn
|
| 4 |
-
pydantic>=2.0.0
|
| 5 |
-
six >= 1.6
|
| 6 |
-
dateutil
|
|
|
|
| 1 |
fastmcp
|
| 2 |
+
python-dateutil
|
| 3 |
+
six
|
| 4 |
fastapi
|
| 5 |
+
uvicorn
|
|
|
|
|
|
|
|
|
run_docker.ps1
CHANGED
|
@@ -1,26 +1,8 @@
|
|
| 1 |
-
cd $PSScriptRoot
|
| 2 |
$ErrorActionPreference = "Stop"
|
| 3 |
-
|
| 4 |
-
$
|
| 5 |
-
$
|
| 6 |
-
$
|
| 7 |
-
|
| 8 |
-
if (!(Test-Path $mcpDir)) { New-Item -ItemType Directory -Path $mcpDir | Out-Null }
|
| 9 |
-
$config = @{}
|
| 10 |
-
if (Test-Path $mcpPath) {
|
| 11 |
-
try { $config = Get-Content $mcpPath -Raw | ConvertFrom-Json } catch { $config = @{} }
|
| 12 |
-
}
|
| 13 |
-
$serversOrdered = [ordered]@{}
|
| 14 |
-
if ($config -and ($config.PSObject.Properties.Name -contains "mcpServers") -and $config.mcpServers) {
|
| 15 |
-
$existing = $config.mcpServers
|
| 16 |
-
if ($existing -is [pscustomobject]) {
|
| 17 |
-
foreach ($p in $existing.PSObject.Properties) { if ($p.Name -ne $entryName) { $serversOrdered[$p.Name] = $p.Value } }
|
| 18 |
-
} elseif ($existing -is [System.Collections.IDictionary]) {
|
| 19 |
-
foreach ($k in $existing.Keys) { if ($k -ne $entryName) { $serversOrdered[$k] = $existing[$k] } }
|
| 20 |
-
}
|
| 21 |
-
}
|
| 22 |
-
$serversOrdered[$entryName] = @{ url = $entryUrl }
|
| 23 |
-
$config = @{ mcpServers = $serversOrdered }
|
| 24 |
-
$config | ConvertTo-Json -Depth 10 | Set-Content -Path $mcpPath -Encoding UTF8
|
| 25 |
docker build -t $imageName .
|
| 26 |
-
docker run --rm -p
|
|
|
|
|
|
|
| 1 |
$ErrorActionPreference = "Stop"
|
| 2 |
+
|
| 3 |
+
$portConfig = Get-Content -Raw -Path "port.json" | ConvertFrom-Json
|
| 4 |
+
$port = $portConfig.port
|
| 5 |
+
$imageName = "dateutil-mcp"
|
| 6 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
docker build -t $imageName .
|
| 8 |
+
docker run --rm -p "${port}:${port}" -e MCP_TRANSPORT=http -e MCP_PORT=$port $imageName
|
run_docker.sh
CHANGED
|
@@ -1,75 +1,8 @@
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
set -euo pipefail
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
if command -v python3 >/dev/null 2>&1; then
|
| 10 |
-
python3 - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
|
| 11 |
-
import json, os, sys
|
| 12 |
-
path, name, url = sys.argv[1:4]
|
| 13 |
-
cfg = {"mcpServers": {}}
|
| 14 |
-
if os.path.exists(path):
|
| 15 |
-
try:
|
| 16 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 17 |
-
cfg = json.load(f)
|
| 18 |
-
except Exception:
|
| 19 |
-
cfg = {"mcpServers": {}}
|
| 20 |
-
if not isinstance(cfg, dict):
|
| 21 |
-
cfg = {"mcpServers": {}}
|
| 22 |
-
servers = cfg.get("mcpServers")
|
| 23 |
-
if not isinstance(servers, dict):
|
| 24 |
-
servers = {}
|
| 25 |
-
ordered = {}
|
| 26 |
-
for k, v in servers.items():
|
| 27 |
-
if k != name:
|
| 28 |
-
ordered[k] = v
|
| 29 |
-
ordered[name] = {"url": url}
|
| 30 |
-
cfg = {"mcpServers": ordered}
|
| 31 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 32 |
-
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
| 33 |
-
PY
|
| 34 |
-
elif command -v python >/dev/null 2>&1; then
|
| 35 |
-
python - "${mcp_path}" "${mcp_entry_name}" "${mcp_entry_url}" <<'PY'
|
| 36 |
-
import json, os, sys
|
| 37 |
-
path, name, url = sys.argv[1:4]
|
| 38 |
-
cfg = {"mcpServers": {}}
|
| 39 |
-
if os.path.exists(path):
|
| 40 |
-
try:
|
| 41 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 42 |
-
cfg = json.load(f)
|
| 43 |
-
except Exception:
|
| 44 |
-
cfg = {"mcpServers": {}}
|
| 45 |
-
if not isinstance(cfg, dict):
|
| 46 |
-
cfg = {"mcpServers": {}}
|
| 47 |
-
servers = cfg.get("mcpServers")
|
| 48 |
-
if not isinstance(servers, dict):
|
| 49 |
-
servers = {}
|
| 50 |
-
ordered = {}
|
| 51 |
-
for k, v in servers.items():
|
| 52 |
-
if k != name:
|
| 53 |
-
ordered[k] = v
|
| 54 |
-
ordered[name] = {"url": url}
|
| 55 |
-
cfg = {"mcpServers": ordered}
|
| 56 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 57 |
-
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
| 58 |
-
PY
|
| 59 |
-
elif command -v jq >/dev/null 2>&1; then
|
| 60 |
-
name="${mcp_entry_name}"; url="${mcp_entry_url}"
|
| 61 |
-
if [ -f "${mcp_path}" ]; then
|
| 62 |
-
tmp="$(mktemp)"
|
| 63 |
-
jq --arg name "$name" --arg url "$url" '
|
| 64 |
-
.mcpServers = (.mcpServers // {})
|
| 65 |
-
| .mcpServers as $s
|
| 66 |
-
| ($s | with_entries(select(.key != $name))) as $base
|
| 67 |
-
| .mcpServers = ($base + {($name): {"url": $url}})
|
| 68 |
-
' "${mcp_path}" > "${tmp}" && mv "${tmp}" "${mcp_path}"
|
| 69 |
-
else
|
| 70 |
-
printf '{ "mcpServers": { "%s": { "url": "%s" } } }
|
| 71 |
-
' "$name" "$url" > "${mcp_path}"
|
| 72 |
-
fi
|
| 73 |
-
fi
|
| 74 |
-
docker build -t dateutil-mcp .
|
| 75 |
-
docker run --rm -p 7951:7860 dateutil-mcp
|
|
|
|
| 1 |
#!/usr/bin/env bash
|
| 2 |
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
PORT=$(python3 -c "import json; print(json.load(open('port.json'))['port'])")
|
| 5 |
+
IMAGE_NAME="dateutil-mcp"
|
| 6 |
+
|
| 7 |
+
docker build -t "${IMAGE_NAME}" .
|
| 8 |
+
docker run --rm -p "${PORT}:${PORT}" -e MCP_TRANSPORT=http -e MCP_PORT="${PORT}" "${IMAGE_NAME}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|