feat: time-ui

This commit is contained in:
Timothy Jaeryang Baek
2025-09-18 21:32:13 -05:00
parent 06881380e4
commit bb9fd9c789
7 changed files with 322 additions and 0 deletions
+34
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
I9NqZuYZF2k68nCw
+51
View File
@@ -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
+14
View File
@@ -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. ⏱️📡
+7
View File
@@ -0,0 +1,7 @@
services:
server:
build:
context: .
ports:
- 8000:8000
+208
View File
@@ -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)
+7
View File
@@ -0,0 +1,7 @@
fastapi
uvicorn[standard]
pydantic
python-multipart
pytz
python-dateutil