refactor(easel): remove TagUtil

This commit is contained in:
2026-09-08 23:22:05 +03:00
parent f76f33b501
commit fa0edca522
11 changed files with 131 additions and 205 deletions
+25
View File
@@ -0,0 +1,25 @@
import datetime
from typing import Annotated
from fastapi import Depends, HTTPException, status
def date_parser(date: str) -> datetime.date:
if date == "today":
return datetime.date.today()
elif date == "tomorrow":
return datetime.date.today() + datetime.timedelta(days=1)
elif date.startswith("day-"):
days = int(date.split("-")[-1])
return datetime.date.today() + datetime.timedelta(days=days)
else:
try:
return datetime.date.strptime(date, "%Y-%m-%d")
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"date": f"{exc}"},
) from exc
DateDepends = Annotated[datetime.date, Depends(date_parser)]
+3 -3
View File
@@ -1,5 +1,3 @@
import datetime
from fastapi import APIRouter
from gallery.easel.core import AppRequest
@@ -7,6 +5,8 @@ from gallery.easel.depends.schedule import ScheduleApiDepends
from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Channel, Schedule
from ...depends.date import DateDepends
router = APIRouter(prefix="/schedule", tags=["Schedule"])
@@ -22,6 +22,6 @@ async def find_api_schedule_channels(schedule_api: ScheduleApiDepends, query: st
@router.get("/{provider}/{channel}/{date}")
async def get_api_schedule_channel_schedule(
schedule_api: ScheduleApiDepends, channel: str, date: datetime.date
schedule_api: ScheduleApiDepends, channel: str, date: DateDepends
) -> Schedule:
return await schedule_api.get_schedule(channel, date)
+3 -3
View File
@@ -1,5 +1,3 @@
import datetime
from fastapi import APIRouter
from gallery.easel.core import AppRequest
@@ -7,6 +5,8 @@ from gallery.easel.depends.weather import WeatherApiDepends
from gallery.sketch.weather.api import WeatherApi
from gallery.sketch.weather.model import Location, WeatherResponse
from ...depends.date import DateDepends
router = APIRouter(prefix="/weather", tags=["Weather"])
@@ -21,7 +21,7 @@ async def find_api_weather_locations(weather_api: WeatherApiDepends, query: str)
@router.get("/{provider}/{location}/day/{date}")
async def get_api_weather_day(weather_api: WeatherApiDepends, location: str, date: datetime.date) -> WeatherResponse:
async def get_api_weather_day(weather_api: WeatherApiDepends, location: str, date: DateDepends) -> WeatherResponse:
return await weather_api.get_day(location, date)
@@ -1,59 +0,0 @@
import datetime
from enum import Enum
from typing import NamedTuple
class TagType(str, Enum):
DAY = "day"
DAYS = "days"
class Tag(NamedTuple):
type: TagType
date: datetime.date
days: int = 1
def __str__(self) -> str:
if self.type == TagType.DAY:
today = datetime.date.today()
day = (self.date - today).days
return f"day-{day}"
elif self.type == TagType.DAYS:
return f"days-{self.days}"
else:
raise ValueError(self.type)
class TagUtil:
@classmethod
def parse_tag(cls, tag: str) -> Tag:
if tag == "today":
return Tag(TagType.DAY, datetime.date.today())
elif tag == "tomorrow":
return Tag(TagType.DAY, datetime.date.today() + datetime.timedelta(days=1))
elif tag.startswith("day-"):
days = int(tag.split("-")[-1])
return Tag(
TagType.DAY,
datetime.date.today() + datetime.timedelta(days=days),
)
elif tag.startswith("days-"):
days = int(tag.split("-")[-1])
return Tag(TagType.DAYS, datetime.date.today(), days)
else:
date = datetime.date.strptime(tag, "%Y-%m-%d")
return cls.create_tag(TagType.DAY, date)
@classmethod
def create_tag(
cls,
tag_type: TagType,
date: datetime.date,
day: int = 0,
days: int = 10,
) -> Tag:
if tag_type == TagType.DAY:
return Tag(tag_type, date + datetime.timedelta(days=day))
if tag_type == TagType.DAYS:
return Tag(tag_type, date, days)
raise ValueError(tag_type)
@@ -11,7 +11,6 @@ from gallery.easel.core import AppRequest
from gallery.version import __version__
from ...translation import _
from .tag import TagUtil
ContextProcessor = typing.Callable[[AppRequest], dict[str, typing.Any]]
@@ -32,7 +31,6 @@ def base_context_processor(request: Request) -> dict:
def build_url(
request: Request, *, path: list[str] | None = None, up: int | None = None, query: dict | None = None
) -> str:
print("!", path)
parts = urlsplit(str(request.url))
if up:
path = [".."] * up
@@ -66,7 +64,6 @@ def build_templates(
"version": __version__,
"format_date": format_date,
"datetime": datetime,
"tag_util": TagUtil,
"DATE_FORMAT": "E, d MMMM Y",
"build_url": build_url,
}
-18
View File
@@ -1,18 +0,0 @@
import datetime
from typing import Annotated
from fastapi import Depends
from ..common.utils.tag import Tag, TagType, TagUtil
def _resolve_tag(date: datetime.date | None = None, tag: str | None = None) -> Tag:
if tag:
result = TagUtil.parse_tag(tag)
else:
date = date or datetime.date.today()
result = TagUtil.create_tag(TagType.DAY, date)
return result
TagDepends = Annotated[Tag, Depends(_resolve_tag)]
+7 -11
View File
@@ -7,14 +7,13 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from gallery.easel.core import AppRequest
from gallery.easel.depends.api import api_resolver
from gallery.easel.depends.date import DateDepends
from gallery.easel.depends.schedule import ScheduleApiDepends
from gallery.sketch.bundle import ApiBundle
from gallery.sketch.schedule.api import ScheduleApi
from gallery.sketch.schedule.model import Schedule
from ..common.utils.tag import TagType
from ..common.utils.template import build_templates
from ..depends.tag import TagDepends
from .bundle import GROUP_MAP
from .filters import timedelta_format
@@ -61,10 +60,10 @@ async def get_group_default(group_id: str):
return RedirectResponse(f"{parse.quote(group_id)}/today")
@router.get("/group/{group_id}/{tag}", response_class=HTMLResponse)
async def get_group(request: AppRequest, group_id: str, tag: TagDepends):
@router.get("/group/{group_id}/{date}", response_class=HTMLResponse)
async def get_group(request: AppRequest, group_id: str, date: DateDepends):
channels = GROUP_MAP.get(group_id, [tuple(item.split(":")) for item in group_id.split(",")])
responses = await _get_group_schedule(request.app.state.api, channels, tag.date)
responses = await _get_group_schedule(request.app.state.api, channels, date)
return templates.TemplateResponse(
request=request,
name="group.html",
@@ -77,12 +76,9 @@ async def get_channel_default(channel: str):
return RedirectResponse(f"{channel}/today")
@router.get("/{provider}/{channel}/{tag}", response_class=HTMLResponse)
async def get_channel_tag(request: AppRequest, schedule_api: ScheduleApiDepends, channel: str, tag: TagDepends):
if tag.type == TagType.DAY:
response = await schedule_api.get_schedule(channel, tag.date)
else:
raise ValueError(tag)
@router.get("/{provider}/{channel}/{date}", response_class=HTMLResponse)
async def get_channel(request: AppRequest, schedule_api: ScheduleApiDepends, channel: str, date: DateDepends):
response = await schedule_api.get_schedule(channel, date)
return templates.TemplateResponse(
request=request,
name="channel.html",
+1 -102
View File
@@ -1,102 +1 @@
import datetime
from pathlib import Path
from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse
from gallery.easel.core import AppRequest
from gallery.easel.depends.api import api_resolver
from gallery.easel.depends.weather import WeatherApiDepends
from gallery.sketch.weather.api import WeatherApi
from gallery.sketch.weather.model import WeatherResponse
from ..common.utils.tag import TagType, TagUtil
from ..common.utils.template import build_templates
from .filters import cloudness_icon, weather_icon_svg, wind_direction_icon
def context_procesor(request: AppRequest) -> dict:
return {
"providers": request.app.state.api.get_api_providers(WeatherApi),
}
templates = build_templates(
Path(__file__).parent / "templates",
{
"wind_direction_icon": wind_direction_icon,
"cloudness_icon": cloudness_icon,
"weather_icon_svg": weather_icon_svg,
},
context_procesor,
)
def build_weather_response(request: AppRequest, response: WeatherResponse):
return templates.TemplateResponse(
request=request,
name="weather.html",
context={
"response": response,
},
)
router = APIRouter(prefix="/weather")
@router.get("/", response_class=HTMLResponse)
async def get_weather_index(request: AppRequest, provider: str | None = None, query: str | None = None):
if query and provider:
weather_api = api_resolver(WeatherApi)(request, provider)
locations = await weather_api.find_locations(query)
else:
locations = []
return templates.TemplateResponse(
request=request,
name="index.html",
context={
"locations": locations,
},
)
@router.get("/{provider}/{location}", response_class=RedirectResponse)
async def get_weather(location: str):
return RedirectResponse(f"{location}/tag/today")
@router.get("/{provider}/{location}/day/{date}", response_class=HTMLResponse)
async def get_weather_day(
request: AppRequest,
weather_api: WeatherApiDepends,
location: str,
date: datetime.date,
):
response = await weather_api.get_day(location, date)
return build_weather_response(request, response)
@router.get("/{provider}/{location}/days/{days}", response_class=HTMLResponse)
async def get_weather_days(request: AppRequest, weather_api: WeatherApiDepends, location: str, days: int):
response = await weather_api.get_days(location, days)
return build_weather_response(request, response)
@router.get("/{provider}/{location}/tag/{tag}", response_class=HTMLResponse)
async def get_weather_tag(request: AppRequest, weather_api: WeatherApiDepends, location: str, tag: str):
tag_value = TagUtil.parse_tag(tag)
if tag_value.type == TagType.DAY:
response = await weather_api.get_day(location, tag_value.date)
elif tag_value.type == TagType.DAYS:
response = await weather_api.get_days(location, tag_value.days)
else:
raise ValueError(tag)
return build_weather_response(request, response)
@router.get("/zmiyevka-184640/tag/{tag}", response_class=RedirectResponse)
async def get_zmiyevka_weather_compat(request: AppRequest):
path_parts = request.url.path.split("/")
path_parts.insert(2, "gismeteo")
return RedirectResponse("/".join(path_parts))
from .route import router
+81
View File
@@ -0,0 +1,81 @@
from pathlib import Path
from typing import Literal
from fastapi import APIRouter
from fastapi.responses import HTMLResponse, RedirectResponse
from gallery.easel.core import AppRequest
from gallery.easel.depends.api import api_resolver
from gallery.easel.depends.date import DateDepends
from gallery.easel.depends.weather import WeatherApiDepends
from gallery.sketch.weather.api import WeatherApi
from gallery.sketch.weather.model import WeatherResponse
from ..common.utils.template import build_templates
from .filters import cloudness_icon, weather_icon_svg, wind_direction_icon
def context_procesor(request: AppRequest) -> dict:
return {
"providers": request.app.state.api.get_api_providers(WeatherApi),
}
templates = build_templates(
Path(__file__).parent / "templates",
{
"wind_direction_icon": wind_direction_icon,
"cloudness_icon": cloudness_icon,
"weather_icon_svg": weather_icon_svg,
},
context_procesor,
)
def build_weather_response(request: AppRequest, response: WeatherResponse):
return templates.TemplateResponse(
request=request,
name="weather.html",
context={"response": response},
)
router = APIRouter(prefix="/weather")
@router.get("/", response_class=HTMLResponse)
async def get_weather_index(request: AppRequest, provider: str | None = None, query: str | None = None):
if query and provider:
weather_api = api_resolver(WeatherApi)(request, provider)
locations = await weather_api.find_locations(query)
else:
locations = []
return templates.TemplateResponse(
request=request,
name="index.html",
context={"locations": locations},
)
@router.get("/{provider}/{location}", response_class=RedirectResponse)
async def get_weather(location: str):
return RedirectResponse(f"{location}/day/today")
@router.get("/{provider}/{location}/days/{days}", response_class=HTMLResponse)
async def get_weather_days(request: AppRequest, weather_api: WeatherApiDepends, location: str, days: int):
response = await weather_api.get_days(location, days)
return build_weather_response(request, response)
# TODO: tag mode route deprecated, used in tizen app
@router.get("/{provider}/{location}/{mode:str}/{date}", response_class=HTMLResponse)
async def get_weather_day(
request: AppRequest,
weather_api: WeatherApiDepends,
location: str,
date: DateDepends,
mode: Literal["day", "tag"], # pylint:disable=unused-argument
):
response = await weather_api.get_day(location, date)
return build_weather_response(request, response)
@@ -1,5 +1,8 @@
{% extends "base.html" %}
{% block title %}{{_("Weather")}} | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}{% endblock %}
{% set date = response.date %}
{% block title %}{{_("Weather")}} | {{response.location}} | {{format_date(response.date, DATE_FORMAT,
locale=request.state.language)}}{% endblock %}
{% block header %}
<app-link href="/weather"
@@ -9,18 +12,18 @@
{% block content %}
<h4>
{% if response.period == 'day' %}
<a class="icon-link {{'disabled' if response.date == datetime.date.today() else ''}}"
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">
<a class="icon-link {{'disabled' if date == datetime.date.today() else ''}}"
href="{{build_url(request, path=['..', date - datetime.timedelta(days=1)])}}">
<i class="bi bi-arrow-left-square"></i>
</a>
<a class="icon-link"
href="../tag/days-10">
href="{{build_url(request, path=['..', '..', 'days', '10'])}}">
<i class="bi bi-arrow-up-square"></i>
</a>
<span class="badge rounded-pill text-bg-secondary fs-6">{{provider}}</span>
<span>{{response.location}} | {{format_date(response.date, DATE_FORMAT, locale=request.state.language)}}</span>
<a class="icon-link"
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">
href="{{build_url(request, path=['..', date + datetime.timedelta(days=1)])}}">
<i class="bi bi-arrow-right-square"></i>
</a>
{% endif %}
@@ -47,7 +50,7 @@
{% if response.period == 'days' %}
<td class="date {{'now' if value.date.date() == datetime.date.today() else ''}}">
<span class="value {{'text-danger' if value.date.weekday() in [5,6] else ''}}">
<a href="../tag/{{tag_util.create_tag('day', value.date.date())}}">
<a href="{{build_url(request, path=['..', '..', 'day', value.date.date()])}}">
{{format_date(value.date, 'E d', locale=request.state.language)}}
</a>
</span>
+2
View File
@@ -71,6 +71,8 @@ class RP5WeatherApi(WeatherApi):
for response in forecast:
if response.date == date:
return response
if date == datetime.date.today():
return WeatherResponse(location=location_id, date=date, period="day", values=[])
raise ValueError(f"Unsupported date: {date}")
async def get_days(self, location_id: str, days: int) -> WeatherResponse: