feat: time-ui
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/go/build-context-dockerignore/
|
||||
|
||||
**/.DS_Store
|
||||
**/__pycache__
|
||||
**/.venv
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose.y*ml
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
@@ -0,0 +1 @@
|
||||
I9NqZuYZF2k68nCw
|
||||
@@ -0,0 +1,51 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Comments are provided throughout this file to help you get started.
|
||||
# If you need more help, visit the Dockerfile reference guide at
|
||||
# https://docs.docker.com/go/dockerfile-reference/
|
||||
|
||||
# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7
|
||||
|
||||
ARG PYTHON_VERSION=3.10.12
|
||||
FROM python:${PYTHON_VERSION}-slim as base
|
||||
|
||||
# Prevents Python from writing pyc files.
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Keeps Python from buffering stdout and stderr to avoid situations where
|
||||
# the application crashes without emitting any logs due to buffering.
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Create a non-privileged user that the app will run under.
|
||||
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||
ARG UID=10001
|
||||
RUN adduser \
|
||||
--disabled-password \
|
||||
--gecos "" \
|
||||
--home "/nonexistent" \
|
||||
--shell "/sbin/nologin" \
|
||||
--no-create-home \
|
||||
--uid "${UID}" \
|
||||
appuser
|
||||
|
||||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||||
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
||||
# into this layer.
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
--mount=type=bind,source=requirements.txt,target=requirements.txt \
|
||||
python -m pip install -r requirements.txt
|
||||
|
||||
# Switch to the non-privileged user to run the application.
|
||||
USER appuser
|
||||
|
||||
# Copy the source code into the container.
|
||||
COPY . .
|
||||
|
||||
# Expose the port that the application listens on.
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application.
|
||||
CMD uvicorn 'main:app' --host=0.0.0.0 --port=8000
|
||||
@@ -0,0 +1,14 @@
|
||||
# 🕒 Time Tool Server
|
||||
|
||||
Blazingly fast time API server ⚡️
|
||||
|
||||
## 🚀 Quickstart
|
||||
|
||||
```bash
|
||||
git clone https://github.com/open-webui/openapi-servers
|
||||
cd openapi-servers/servers/time
|
||||
pip install -r requirements.txt
|
||||
uvicorn main:app --host 0.0.0.0 --reload
|
||||
```
|
||||
|
||||
You're live. ⏱️📡
|
||||
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- 8000:8000
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
from fastapi import FastAPI, HTTPException, Body
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Literal
|
||||
import pytz
|
||||
from dateutil import parser as dateutil_parser
|
||||
|
||||
app = FastAPI(
|
||||
title="Secure Time Utilities API",
|
||||
version="1.0.0",
|
||||
description="Provides secure UTC/local time retrieval, formatting, timezone conversion, and comparison.",
|
||||
)
|
||||
|
||||
origins = ["*"]
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------
|
||||
# Pydantic models
|
||||
# -------------------------------
|
||||
class FormatTimeInput(BaseModel):
|
||||
format: str = Field(
|
||||
"%Y-%m-%d %H:%M:%S", description="Python strftime format string"
|
||||
)
|
||||
timezone: str = Field(
|
||||
"UTC", description="IANA timezone name (e.g., UTC, America/New_York)"
|
||||
)
|
||||
|
||||
|
||||
class ConvertTimeInput(BaseModel):
|
||||
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):
|
||||
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):
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
# -------------------------------
|
||||
# Helper for pretty HTML
|
||||
# -------------------------------
|
||||
def html_page(title: str, body: str) -> HTMLResponse:
|
||||
html = f"""
|
||||
<html>
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
background-color: #f7f7f7;
|
||||
height: 4rem;
|
||||
color: #333;
|
||||
}}
|
||||
h1 {{
|
||||
color: #2c3e50;
|
||||
}}
|
||||
pre {{
|
||||
background: #ecf0f1;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 1.1em;
|
||||
}}
|
||||
ul {{
|
||||
columns: 3;
|
||||
-webkit-columns: 3;
|
||||
-moz-columns: 3;
|
||||
}}
|
||||
li {{
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{title}</h1>
|
||||
{body}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html
|
||||
|
||||
|
||||
# -------------------------------
|
||||
# Routes
|
||||
# -------------------------------
|
||||
@app.get("/get_current_local_time", summary="Current Local Time")
|
||||
def get_current_local():
|
||||
local_time = datetime.now().isoformat()
|
||||
headers = {"Content-Disposition": "inline"}
|
||||
return HTMLResponse(
|
||||
content=html_page("Current Local Time", f"<pre>{local_time}</pre>"),
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/format_time", summary="Format current time")
|
||||
def format_current_time(data: FormatTimeInput):
|
||||
try:
|
||||
tz = pytz.timezone(data.timezone)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Invalid timezone: {data.timezone}"
|
||||
)
|
||||
now = datetime.now(tz)
|
||||
try:
|
||||
formatted = now.strftime(data.format)
|
||||
headers = {"Content-Disposition": "inline"}
|
||||
return HTMLResponse(
|
||||
content=html_page("Formatted Time", f"<pre>{formatted}</pre>"),
|
||||
headers=headers,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid format string: {e}")
|
||||
|
||||
|
||||
@app.post("/convert_time", summary="Convert between timezones")
|
||||
def convert_time(data: ConvertTimeInput):
|
||||
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}")
|
||||
try:
|
||||
dt = dateutil_parser.parse(data.timestamp)
|
||||
if dt.tzinfo is None:
|
||||
dt = from_zone.localize(dt)
|
||||
else:
|
||||
dt = dt.astimezone(from_zone)
|
||||
converted = dt.astimezone(to_zone).isoformat()
|
||||
headers = {"Content-Disposition": "inline"}
|
||||
return HTMLResponse(
|
||||
content=html_page("Converted Time", f"<pre>{converted}</pre>"),
|
||||
headers=headers,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid timestamp: {e}")
|
||||
|
||||
|
||||
@app.post("/parse_timestamp", summary="Parse and normalize timestamps")
|
||||
def parse_timestamp(data: ParseTimestampInput):
|
||||
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).isoformat()
|
||||
headers = {"Content-Disposition": "inline"}
|
||||
return HTMLResponse(
|
||||
content=html_page("Parsed Timestamp (UTC)", f"<pre>{dt_utc}</pre>"),
|
||||
headers=headers,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=f"Could not parse: {e}")
|
||||
|
||||
|
||||
@app.get("/list_time_zones", summary="All valid time zones")
|
||||
def list_time_zones():
|
||||
tz_list_html = (
|
||||
"<ul>" + "".join(f"<li>{tz}</li>" for tz in pytz.all_timezones) + "</ul>"
|
||||
)
|
||||
headers = {"Content-Disposition": "inline"}
|
||||
return HTMLResponse(
|
||||
content=html_page("Valid Time Zones", tz_list_html), headers=headers
|
||||
)
|
||||
|
||||
|
||||
@app.get("/go_to_timezones", summary="Redirect to /list_time_zones")
|
||||
def redirect_to_timezones():
|
||||
"""
|
||||
Redirects user to the list of time zones.
|
||||
"""
|
||||
return RedirectResponse(url="/list_time_zones")
|
||||
|
||||
|
||||
@app.get("/useful_redirect", summary="Redirect to useful external time resource")
|
||||
def useful_redirect():
|
||||
"""
|
||||
Redirects users to a useful external website (timeanddate.com).
|
||||
"""
|
||||
headers = {"Content-Disposition": "inline"}
|
||||
external_url = "https://time.is"
|
||||
return RedirectResponse(url=external_url, headers=headers)
|
||||
@@ -0,0 +1,7 @@
|
||||
fastapi
|
||||
uvicorn[standard]
|
||||
pydantic
|
||||
python-multipart
|
||||
|
||||
pytz
|
||||
python-dateutil
|
||||
Reference in New Issue
Block a user