Compare commits

...

3 commits

Author SHA1 Message Date
8e6cb53669
Add ForgeJo registry push 2025-10-04 21:17:01 -04:00
47c1a840c9
Add timezone support 2025-10-04 17:29:42 -04:00
86756b7eb8
Move code to go 2021-10-14 16:05:56 -04:00
14 changed files with 141 additions and 101 deletions

40
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,40 @@
default:
image: $CI_REGISTRY/imageroot/buildah:latest
stages:
- build
.container:
before_script:
- podman login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY <<<$CI_REGISTRY_PASSWORD
- podman login -u $NEXUS_USERNAME --password-stdin $NEXUS_DOCKER_IO <<<$NEXUS_PASSWORD
- podman login -u $FORGEJO_PACKAGES_USER --password-stdin $FORGEJO_PACKAGES_DOMAIN <<<$FORGEJO_PACKAGES_TOKEN
build-master-container:
extends: .container
stage: build
script:
- podman build --build-arg DOCKER_IO=${NEXUS_DOCKER_IO} -t ${CI_REGISTRY_IMAGE}:master .
- podman push ${CI_REGISTRY_IMAGE}:master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
build-tagged-container:
extends: .container
stage: build
variables:
FORGEJO_IMAGE: git.x59.dev/x59/time-go
script:
- podman build
--build-arg DOCKER_IO=${NEXUS_DOCKER_IO}
-t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}
-t ${CI_REGISTRY_IMAGE}:latest
-t ${FORGEJO_IMAGE}:${CI_COMMIT_TAG}
-t ${FORGEJO_IMAGE}:latest
.
- podman push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}
- podman push ${CI_REGISTRY_IMAGE}:latest
- podman push ${FORGEJO_IMAGE}:${CI_COMMIT_TAG}
- podman push ${FORGEJO_IMAGE}:latest
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+/

View file

@ -1,15 +1,16 @@
FROM python:3.9-rc-alpine ARG DOCKER_IO=docker.io
MAINTAINER Yehuda Deutsch <yeh@uda.co.il> FROM ${DOCKER_IO}/library/golang:1.24-alpine AS build
RUN apk add tzdata \
&& apk add --virtual .build-deps gcc libc-dev make \
&& pip install fastapi uvicorn \
&& rm -Rf ~/.cache \
&& apk --purge del .build-deps gcc libc-dev make
WORKDIR /code WORKDIR /code
ENV PYTHONPATH=/code
COPY time_app/ /code/time_app/
CMD ["python", "time_app"] COPY time.go /code
RUN go build ./time.go
FROM ${DOCKER_IO}/library/alpine:latest AS final
RUN apk add --no-cache tzdata
WORKDIR /code
COPY --from=build /code/time /code/time
CMD ["/code/time"]

89
time.go Normal file
View file

@ -0,0 +1,89 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
func getTime(location string) (time.Time, error) {
loc, err := time.LoadLocation(location)
if err != nil {
return time.Now(), err
}
return time.Now().In(loc), err
}
var zoneDir string
func loadZoneLocations(path string) []string {
var locations []string
zoneFiles, _ := os.ReadDir("/usr/share/zoneinfo/" + path)
for _, file := range zoneFiles {
if file.IsDir() {
locations = append(locations, loadZoneLocations(path+"/"+file.Name())...)
} else {
if _, err := time.LoadLocation((path + "/" + file.Name())[1:]); err != nil {
continue
}
locations = append(locations, (path + "/" + file.Name())[1:])
}
}
return locations
}
var runtimeLocations = loadZoneLocations(zoneDir)
func mainHandler(w http.ResponseWriter, r *http.Request) {
now, err := getTime("UTC")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Could not get current time")
return
}
fmt.Fprintf(w, now.Format("2006-01-02 15:04:05"))
}
func dayInYearHandler(w http.ResponseWriter, r *http.Request) {
now, err := getTime("UTC")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Could not get current time")
return
}
fmt.Fprintf(w, "%s%d", string(now.Format("06")[1]), now.YearDay())
}
func timeAtZoneHandler(w http.ResponseWriter, r *http.Request) {
timezone := r.PathValue("timezone")
now, err := getTime(timezone)
if err != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, "Timezone not found")
return
}
fmt.Fprintf(w, now.Format("2006-01-02 15:04:05"))
}
func ZoneListHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(runtimeLocations)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Could not encode zones")
}
}
func main() {
http.HandleFunc("GET /", mainHandler)
http.HandleFunc("GET /diy", dayInYearHandler)
http.HandleFunc("GET /at/{timezone...}", timeAtZoneHandler)
http.HandleFunc("GET /timezones", ZoneListHandler)
listen := ":8000"
fmt.Println("Listening on " + listen)
http.ListenAndServe(listen, nil)
}

View file

@ -1 +0,0 @@

View file

@ -1,3 +0,0 @@
from time_app.main import run
run()

View file

@ -1,21 +0,0 @@
from datetime import datetime
from zoneinfo import ZoneInfo, available_timezones
ALLOWED_TIMEZONES = available_timezones()
DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
def tz_aware_now(zone: str) -> datetime:
"""
:param str zone: Zone name in DB
:return: A timezone aware datetime object
:raises: ValueError When zone name is not supported
"""
# ZoneInfo uses a weak cache, no need to implement a local cache
if zone not in ALLOWED_TIMEZONES:
raise ValueError(f'Unsupported timezone "{zone}"')
return datetime.now(tz=ZoneInfo(zone))
def real_utc_now() -> datetime:
return tz_aware_now('UTC')

View file

@ -1,11 +0,0 @@
from fastapi import HTTPException
from .datetime import tz_aware_now
def get_now_in_view(zone: str = 'UTC'):
try:
now = tz_aware_now(zone=zone)
except ValueError:
raise HTTPException(status_code=400, detail=f'Zone "{zone}" is unsupported')
return now

View file

@ -1,13 +0,0 @@
import uvicorn
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
from .views import main, api
app = FastAPI()
app.include_router(main.router, tags=['text'], default_response_class=PlainTextResponse)
app.include_router(api.router, prefix='/api/v1', tags=['api'])
def run():
uvicorn.run("time_app.main:app", host="0.0.0.0", port=8000, log_level="info")

View file

@ -1,5 +0,0 @@
from pydantic import BaseModel
class Now(BaseModel):
time: str

View file

@ -1,16 +0,0 @@
from fastapi import APIRouter
from time_app.helpers.views import get_now_in_view
from time_app.models.main import Now
router = APIRouter()
@router.get('/now', name='Now', response_model=Now)
def now_view(zone: str = 'UTC') -> dict:
return {"time": f'{get_now_in_view(zone):%Y-%m-%d %H:%M:%S}'}
@router.get('/diy', name='Day in year', response_model=Now)
def day_in_year_view(zone: str = 'UTC') -> dict:
return {"time": f'{get_now_in_view(zone):%y%j}'[-4:]}

View file

@ -1,20 +0,0 @@
from fastapi import APIRouter
from time_app.helpers.views import get_now_in_view
router = APIRouter()
@router.get('/', name='Now')
def now_view() -> str:
return f'{get_now_in_view():%Y-%m-%d %H:%M:%S}'
@router.get('/now', name='Now')
def now_view(zone: str = 'UTC') -> str:
return f'{get_now_in_view(zone):%Y-%m-%d %H:%M:%S}'
@router.get('/diy', name='Day in year')
def day_in_year_view(zone: str = 'UTC') -> str:
return f'{get_now_in_view(zone):%y%j}'[-4:]