Compare commits
15 Commits
d3ef03a6a0
...
0.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| c233b020fc | |||
| 869a8ae79f | |||
| 4c3b3aeafc | |||
| d1592150fd | |||
| 9351b9f53a | |||
| ecb574e286 | |||
| 94870a5c86 | |||
| 3dd0a5410c | |||
| a0e6f30e3b | |||
| 29fa6435ce | |||
| a886322d0e | |||
| 6112147b40 | |||
| ad8144df37 | |||
| f303d0e1f4 | |||
| 3e80ccb0df |
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
5
.env
Normal file
5
.env
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
DOCKER_REPO=git.shmyga.ru
|
||||||
|
DOCKER_GROUP=infernalgames
|
||||||
|
DOCKER_ROOT="$DOCKER_REPO/$DOCKER_GROUP"
|
||||||
|
VERSION=$(grep -m 1 'version' ./pyproject.toml | grep -oP 'version\s*=\s*"\K[^"]+')
|
||||||
|
DOCKER_PROJECTS=("gallery")
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,7 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
|
*.mo
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
.venv
|
.venv
|
||||||
#.vscode
|
#.vscode
|
||||||
|
static/node_modules
|
||||||
|
static/dist
|
||||||
16
Dockerfile
16
Dockerfile
@@ -3,21 +3,31 @@ ENV POETRY_HOME="/opt/poetry"
|
|||||||
ENV PATH="$POETRY_HOME/bin:$PATH"
|
ENV PATH="$POETRY_HOME/bin:$PATH"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
COPY pyproject.toml poetry.lock ./
|
COPY pyproject.toml poetry.lock README.md ./
|
||||||
RUN poetry config virtualenvs.in-project true
|
RUN poetry config virtualenvs.in-project true
|
||||||
RUN poetry install --with app
|
RUN poetry install --with app --no-root
|
||||||
|
|
||||||
|
FROM node:24 AS node-builder
|
||||||
|
ENV PATH=/app/node_modules/.bin:$PATH
|
||||||
|
WORKDIR /app
|
||||||
|
COPY static/package.json static/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY static ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
ENV PATH="/app/.venv/bin:$PATH"
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
apt install -y locales && \
|
apt install -y locales gettext && \
|
||||||
sed -i -e 's/# ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen && \
|
sed -i -e 's/# ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen && \
|
||||||
dpkg-reconfigure --frontend=noninteractive locales
|
dpkg-reconfigure --frontend=noninteractive locales
|
||||||
ENV LANG=ru_RU.UTF-8
|
ENV LANG=ru_RU.UTF-8
|
||||||
ENV LC_ALL=ru_RU.UTF-8
|
ENV LC_ALL=ru_RU.UTF-8
|
||||||
ENV TZ="Europe/Moscow"
|
ENV TZ="Europe/Moscow"
|
||||||
COPY --from=builder /app ./
|
COPY --from=builder /app ./
|
||||||
|
COPY --from=node-builder /app/dist ./static/dist
|
||||||
COPY gallery gallery/
|
COPY gallery gallery/
|
||||||
|
RUN cd gallery/easel/route/view/locales/ru/LC_MESSAGES && msgfmt messages.po
|
||||||
|
|
||||||
CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"]
|
CMD ["uvicorn", "gallery.main:app", "--host", "0.0.0.0", "--port", "80", "--log-config", "gallery/logging.yaml"]
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -1 +1,9 @@
|
|||||||
# Gallery
|
# API Gallery
|
||||||
|
|
||||||
|
## View
|
||||||
|
|
||||||
|
https://api.shmyga.ru
|
||||||
|
|
||||||
|
## Swagger
|
||||||
|
|
||||||
|
https://api.shmyga.ru/docs
|
||||||
|
|||||||
25
docker-compose-develop.yaml
Normal file
25
docker-compose-develop.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: gallery
|
||||||
|
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
container_name: gallery-redis
|
||||||
|
image: redis:alpine
|
||||||
|
stop_grace_period: 3s
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
app:
|
||||||
|
container_name: gallery-app-develop
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- DEBUG=1
|
||||||
|
ports:
|
||||||
|
- 8000:80
|
||||||
|
develop:
|
||||||
|
watch:
|
||||||
|
- action: sync
|
||||||
|
path: ./gallery
|
||||||
|
target: /app/gallery
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data:
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
name: gallery
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
container_name: gallery-redis
|
container_name: gallery-redis
|
||||||
@@ -5,15 +7,15 @@ services:
|
|||||||
stop_grace_period: 3s
|
stop_grace_period: 3s
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
command: [ "redis-server", "--bind", "0.0.0.0", "--port", "6379" ]
|
|
||||||
app:
|
app:
|
||||||
container_name: gallery-app
|
container_name: gallery-app
|
||||||
build: .
|
image: ${DOCKER_ROOT}/gallery
|
||||||
# image: shmyga/gallery
|
|
||||||
environment:
|
environment:
|
||||||
- REDIS_HOST=redis
|
- REDIS_HOST=redis
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
ports:
|
ports:
|
||||||
- 8000:80
|
- 127.0.0.1:8000:80
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
{
|
{
|
||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"path": "."
|
"path": ".",
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"python.testing.pytestArgs": ["tests", "-s"],
|
"python.testing.pytestArgs": ["tests", "-s"],
|
||||||
"python.testing.unittestEnabled": false,
|
"python.testing.unittestEnabled": false,
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
|
"python-envs.pythonProjects": [
|
||||||
|
{
|
||||||
|
"path": ".",
|
||||||
|
"envManager": "ms-python.python:poetry",
|
||||||
|
"packageManager": "ms-python.python:poetry",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"files.associations": {
|
||||||
|
"*.html": "jinja-html",
|
||||||
|
},
|
||||||
"files.exclude": {
|
"files.exclude": {
|
||||||
"**/__pycache__": true
|
"**/__pycache__": true,
|
||||||
},
|
},
|
||||||
"terminal.integrated.env.linux": {
|
"terminal.integrated.env.linux": {
|
||||||
"PYTHONPATH": "${workspaceFolder}"
|
"PYTHONPATH": "${workspaceFolder}",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
"launch": {
|
"launch": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
@@ -23,13 +33,15 @@
|
|||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "uvicorn",
|
"module": "uvicorn",
|
||||||
"args": [
|
"args": ["gallery.main:app", "--reload", "--log-config", "gallery/logging.yaml"],
|
||||||
"gallery.main:app",
|
},
|
||||||
"--reload",
|
{
|
||||||
"--log-config",
|
"name": "gallery:static",
|
||||||
"gallery/logging.yaml"
|
"cwd": "${workspaceFolder}/static",
|
||||||
]
|
"request": "launch",
|
||||||
}
|
"type": "node-terminal",
|
||||||
]
|
"command": "npm run dev",
|
||||||
}
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,27 @@
|
|||||||
import locale as _locale
|
import locale as _locale
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from gallery.sketch.schedule.api import ScheduleApi
|
from gallery.sketch.bundle import ApiBundle
|
||||||
from gallery.sketch.weather.api import WeatherApi
|
from gallery.util import root_path
|
||||||
|
|
||||||
from .route import doc
|
from .route import api, doc
|
||||||
from .route.api import schedule as schedule_api_route
|
from .route.view import router as view_router
|
||||||
from .route.api import weather as weather_api_route
|
|
||||||
from .route.view import common as common_view_route
|
DEFAULT_LOCALE = "ru_RU.UTF-8"
|
||||||
from .route.view import schedule as schedule_view_route
|
|
||||||
from .route.view import weather as weather_view_route
|
|
||||||
|
|
||||||
|
|
||||||
def build_app(
|
def build_app(api_bundle: ApiBundle, *, locale: str = DEFAULT_LOCALE) -> FastAPI:
|
||||||
weather_api: WeatherApi, schedule_api: ScheduleApi, *, locale: str = "ru_RU.UTF-8"
|
|
||||||
) -> FastAPI:
|
|
||||||
_locale.setlocale(_locale.LC_TIME, locale)
|
_locale.setlocale(_locale.LC_TIME, locale)
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Gallery",
|
title="Gallery",
|
||||||
docs_url=None,
|
docs_url=None,
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
)
|
)
|
||||||
app.state.weather_api = weather_api
|
app.state.api = api_bundle
|
||||||
app.state.schedule_api = schedule_api
|
app.mount("/static", StaticFiles(directory=root_path / "static/dist"))
|
||||||
doc.mount(app)
|
doc.mount(app)
|
||||||
weather_api_route.mount(app)
|
api.mount(app)
|
||||||
schedule_api_route.mount(app)
|
app.include_router(view_router)
|
||||||
common_view_route.mount(app)
|
|
||||||
weather_view_route.mount(app)
|
|
||||||
schedule_view_route.mount(app)
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
15
gallery/easel/core.py
Normal file
15
gallery/easel/core.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
from gallery.sketch.bundle import ApiBundle
|
||||||
|
|
||||||
|
|
||||||
|
class State:
|
||||||
|
api: ApiBundle
|
||||||
|
|
||||||
|
|
||||||
|
class App:
|
||||||
|
state: State
|
||||||
|
|
||||||
|
|
||||||
|
class AppRequest(Request):
|
||||||
|
app: App
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from . import schedule, weather
|
||||||
|
|
||||||
|
|
||||||
|
def mount(app: FastAPI):
|
||||||
|
weather.mount(app)
|
||||||
|
schedule.mount(app)
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from gallery.easel.core import AppRequest
|
||||||
|
from gallery.sketch.schedule.model import ChannelId, Schedule
|
||||||
|
|
||||||
|
|
||||||
def mount(app: FastAPI):
|
def mount(app: FastAPI):
|
||||||
pass
|
@app.get("/api/schedule/channels", tags=["API"])
|
||||||
|
async def get_api_schedule_channels(request: AppRequest) -> list[ChannelId]:
|
||||||
|
schedule_api = request.app.state.api.schedule
|
||||||
|
return await schedule_api.get_channels()
|
||||||
|
|
||||||
|
@app.get("/api/schedule/{channel}/{date}", tags=["API"])
|
||||||
|
async def get_api_schedule_channel_schedule(
|
||||||
|
request: AppRequest, channel: str, date: datetime.date
|
||||||
|
) -> Schedule:
|
||||||
|
schedule_api = request.app.state.api.schedule
|
||||||
|
return await schedule_api.get_channel_schedule(ChannelId(channel), date)
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from gallery.sketch.weather.api import WeatherApi
|
from gallery.easel.core import AppRequest
|
||||||
from gallery.sketch.weather.model import WeatherResponse
|
from gallery.sketch.weather.model import Location, WeatherResponse
|
||||||
|
|
||||||
|
|
||||||
def mount(app: FastAPI):
|
def mount(app: FastAPI):
|
||||||
@app.get("/api/weather/locations")
|
@app.get("/api/weather/locations", tags=["API"])
|
||||||
async def get_api_weather_locations(request: Request) -> list[str]:
|
async def get_api_weather_locations(
|
||||||
weather_api: WeatherApi = request.app.state.weather_api
|
request: AppRequest, query: str
|
||||||
return await weather_api.get_locations()
|
) -> list[Location]:
|
||||||
|
weather_api = request.app.state.api.weather
|
||||||
|
return await weather_api.find_locations(query)
|
||||||
|
|
||||||
@app.get("/api/weather/{location}/day/{date}")
|
@app.get("/api/weather/{location}/day/{date}", tags=["API"])
|
||||||
async def get_api_weather_day(
|
async def get_api_weather_day(
|
||||||
request: Request, location: str, date: datetime.date
|
request: AppRequest, location: str, date: datetime.date
|
||||||
) -> WeatherResponse:
|
) -> WeatherResponse:
|
||||||
weather_api: WeatherApi = request.app.state.weather_api
|
weather_api = request.app.state.api.weather
|
||||||
return await weather_api.get_day(location, date)
|
return await weather_api.get_day(location, date)
|
||||||
|
|
||||||
@app.get("/api/weather/{location}/days/{days}")
|
@app.get("/api/weather/{location}/days/{days}", tags=["API"])
|
||||||
async def get_api_weather_days(
|
async def get_api_weather_days(
|
||||||
request: Request, location: str, days: int
|
request: AppRequest, location: str, days: int
|
||||||
) -> WeatherResponse:
|
) -> WeatherResponse:
|
||||||
weather_api: WeatherApi = request.app.state.weather_api
|
weather_api = request.app.state.api.weather
|
||||||
return await weather_api.get_days(location, days)
|
return await weather_api.get_days(location, days)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from .common import router as common_router
|
||||||
|
from .schedule import router as schedule_router
|
||||||
|
from .translation import set_language
|
||||||
|
from .weather import router as weather_router
|
||||||
|
|
||||||
|
router = APIRouter(dependencies=[Depends(set_language)])
|
||||||
|
router.include_router(common_router)
|
||||||
|
router.include_router(weather_router)
|
||||||
|
router.include_router(schedule_router)
|
||||||
|
|||||||
@@ -1,37 +1,41 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import APIRouter, Request
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from gallery.version import __version__
|
from gallery.version import __version__
|
||||||
|
|
||||||
|
from ..translation import _
|
||||||
|
|
||||||
|
|
||||||
class Section(NamedTuple):
|
class Section(NamedTuple):
|
||||||
link: str
|
link: str
|
||||||
title: str
|
title: str
|
||||||
|
icon: str
|
||||||
|
|
||||||
|
|
||||||
SECTIONS = [
|
SECTIONS = [
|
||||||
Section("weather", "Погода"),
|
Section("weather", "Weather", "brightness-high"),
|
||||||
Section("schedule", "Телепрограмма"),
|
Section("schedule", "TV program", "tv"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
base_dir = Path(__file__).parent
|
||||||
|
|
||||||
def mount(app: FastAPI):
|
router = APIRouter()
|
||||||
base_dir = Path(__file__).parent
|
|
||||||
app.mount("/static/common", StaticFiles(directory=base_dir / "static"))
|
|
||||||
templates = Jinja2Templates(directory=base_dir / "templates")
|
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
templates = Jinja2Templates(directory=base_dir / "templates")
|
||||||
async def get_section_list(request: Request):
|
templates.env.globals.update({"_": _})
|
||||||
return templates.TemplateResponse(
|
|
||||||
request=request,
|
|
||||||
name="index.html",
|
@router.get("/", response_class=HTMLResponse)
|
||||||
context={
|
async def get_section_list(request: Request):
|
||||||
"version": __version__,
|
return templates.TemplateResponse(
|
||||||
"sections": SECTIONS,
|
request=request,
|
||||||
},
|
name="root_index.html",
|
||||||
)
|
context={
|
||||||
|
"version": __version__,
|
||||||
|
"sections": SECTIONS,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
base
|
|
||||||
*/
|
|
||||||
body {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
table
|
|
||||||
*/
|
|
||||||
table {
|
|
||||||
table-layout: fixed;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
table,
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 0.1rem 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
a.button
|
|
||||||
*/
|
|
||||||
a.button {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.disabled {
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: default;
|
|
||||||
color: gray;
|
|
||||||
filter: grayscale(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
app
|
|
||||||
*/
|
|
||||||
.app-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-title {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-link-home > * {
|
|
||||||
margin-left: 2rem;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
background-image: url("/static/common/gallery.png");
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
display: inline-block;
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.app-list {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.app-list > li {
|
|
||||||
border: 1px solid lightgrey;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.app-list > li > a {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
padding: 0.5rem 2rem;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.app-list > li:hover {
|
|
||||||
border-color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.app-list > li:hover > a {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
124
gallery/easel/route/view/common/templates/base.html
Normal file
124
gallery/easel/route/view/common/templates/base.html
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{request.state.language}}">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
{% block head %}
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible"
|
||||||
|
content="ie=edge">
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="/static/gallery.css?v={{version}}">
|
||||||
|
<script type="module"
|
||||||
|
src="/static/gallery.es.js?v={{version}}"></script>
|
||||||
|
<link rel="icon"
|
||||||
|
href="/favicon.ico?v={{version}}"
|
||||||
|
type="image/x-icon">
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="app col-lg-8 mx-auto p-3 py-md-5">
|
||||||
|
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
|
||||||
|
<app-link href="/"
|
||||||
|
icon="gear">API Gallery</app-link>
|
||||||
|
{% block header %}{% endblock %}
|
||||||
|
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
|
||||||
|
id="bd-language"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-label="Select language (default)">
|
||||||
|
<span class="me-2 language-icon-active">🇬🇧</span>
|
||||||
|
<span class="d-lg-none ms-2"
|
||||||
|
id="bd-language-text">Select language</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end"
|
||||||
|
aria-labelledby="bd-language-text">
|
||||||
|
<li>
|
||||||
|
<button type="button"
|
||||||
|
class="dropdown-item d-flex align-items-center"
|
||||||
|
data-bs-language-value="en"
|
||||||
|
aria-pressed="false">
|
||||||
|
<span class="me-2 language-icon">🇬🇧</span>
|
||||||
|
English
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button"
|
||||||
|
class="dropdown-item d-flex align-items-center"
|
||||||
|
data-bs-language-value="ru"
|
||||||
|
aria-pressed="false">
|
||||||
|
<span class="me-2 language-icon">🇷🇺</span>
|
||||||
|
Russian
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
|
||||||
|
id="bd-theme"
|
||||||
|
type="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-label="Toggle theme (auto)">
|
||||||
|
<i class="bi me-2 opacity-50 theme-icon-active"></i>
|
||||||
|
<span class="d-lg-none ms-2"
|
||||||
|
id="bd-theme-text">Toggle theme</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end"
|
||||||
|
aria-labelledby="bd-theme-text">
|
||||||
|
<li>
|
||||||
|
<button type="button"
|
||||||
|
class="dropdown-item d-flex align-items-center"
|
||||||
|
data-bs-theme-value="light"
|
||||||
|
aria-pressed="false">
|
||||||
|
<i class="bi bi-sun-fill me-2 opacity-50 theme-icon"></i>
|
||||||
|
Light
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button"
|
||||||
|
class="dropdown-item d-flex align-items-center"
|
||||||
|
data-bs-theme-value="dark"
|
||||||
|
aria-pressed="false">
|
||||||
|
<i class="bi bi-moon-stars-fill me-2 opacity-50 theme-icon"></i>
|
||||||
|
Dark
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button"
|
||||||
|
class="dropdown-item d-flex align-items-center active"
|
||||||
|
data-bs-theme-value="auto"
|
||||||
|
aria-pressed="true">
|
||||||
|
<i class="bi bi-circle-half me-2 opacity-50 theme-icon"></i>
|
||||||
|
Auto
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
<footer class="pt-5 my-5 text-muted border-top">
|
||||||
|
Created by shmyga · © 2026
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const widget = params.get('widget') || window.location.hostname.startsWith('weather');
|
||||||
|
if (widget) {
|
||||||
|
document.body.classList.add('widget');
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible"
|
|
||||||
content="ie=edge">
|
|
||||||
<title>Информация</title>
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/common/style.css?v={{version}}">
|
|
||||||
<link rel="icon"
|
|
||||||
href="/static/common/favicon.ico?v={{version}}"
|
|
||||||
type="image/x-icon">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="app-container">
|
|
||||||
<h3 class="app-header">
|
|
||||||
<a class="app-link-home"
|
|
||||||
href="/">
|
|
||||||
<div></div>
|
|
||||||
</a>
|
|
||||||
<div class="app-title">
|
|
||||||
<span>Информация</span>
|
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
<ul class="app-list">
|
|
||||||
{% for section in sections %}
|
|
||||||
<li>
|
|
||||||
<a href="{{section.link}}">
|
|
||||||
<span class="icon"
|
|
||||||
style="background-image: url(/static/{{section.link}}/{{section.link}}.png);"></span>
|
|
||||||
<span>{{section.title}}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
26
gallery/easel/route/view/common/templates/root_index.html
Normal file
26
gallery/easel/route/view/common/templates/root_index.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Index{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>View</h1>
|
||||||
|
<div class="list-group mb-5">
|
||||||
|
{% for section in sections %}
|
||||||
|
<a href="{{section.link}}"
|
||||||
|
class="list-group-item list-group-item-action px-4">
|
||||||
|
<app-link href="{{section.link}}"
|
||||||
|
icon="{{section.icon}}">
|
||||||
|
{{_(section.title)}}
|
||||||
|
</app-link>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<hr class="col-3 col-md-2 mb-5">
|
||||||
|
<h1>Docs</h1>
|
||||||
|
<a href="/docs"
|
||||||
|
target="_blank">
|
||||||
|
<h4>Swagger</h4>
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
||||||
15
gallery/easel/route/view/locales/ru/LC_MESSAGES/messages.po
Normal file
15
gallery/easel/route/view/locales/ru/LC_MESSAGES/messages.po
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Gallery\n"
|
||||||
|
"Last-Translator: shmyga <shmyga.z@gmail.com>\n"
|
||||||
|
"Language: ru\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
|
|
||||||
|
msgid "Weather"
|
||||||
|
msgstr "Погода"
|
||||||
|
|
||||||
|
msgid "TV program"
|
||||||
|
msgstr "Телепрограмма"
|
||||||
@@ -1,81 +1,85 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import APIRouter
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from gallery.sketch.schedule.api import ScheduleApi
|
from gallery.easel.core import AppRequest
|
||||||
from gallery.sketch.schedule.catalog import BUNDLE
|
from gallery.sketch.schedule.catalog import BUNDLE
|
||||||
from gallery.version import __version__
|
from gallery.version import __version__
|
||||||
|
|
||||||
from ..common.util import TagType, TagUtil
|
from ..common.util import TagType, TagUtil
|
||||||
|
from ..translation import _
|
||||||
from .filters import timedelta_format
|
from .filters import timedelta_format
|
||||||
|
|
||||||
|
base_dir = Path(__file__).parent
|
||||||
|
templates = Jinja2Templates(
|
||||||
|
directory=[
|
||||||
|
base_dir.parent / "common/templates",
|
||||||
|
base_dir / "templates",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
templates.env.globals.update({"_": _})
|
||||||
|
templates.env.filters["timedelta_format"] = timedelta_format
|
||||||
|
|
||||||
def mount(app: FastAPI):
|
router = APIRouter()
|
||||||
base_dir = Path(__file__).parent
|
|
||||||
app.mount("/static/schedule", StaticFiles(directory=base_dir / "static"))
|
|
||||||
templates = Jinja2Templates(directory=base_dir / "templates")
|
|
||||||
templates.env.filters["timedelta_format"] = timedelta_format
|
|
||||||
|
|
||||||
@app.get("/schedule", response_class=HTMLResponse)
|
|
||||||
async def get_schedule_list(request: Request):
|
|
||||||
schedule_api: ScheduleApi = request.app.state.schedule_api
|
|
||||||
channels = await schedule_api.get_channels()
|
|
||||||
channels_data = BUNDLE.select_items(channels)
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
request=request,
|
|
||||||
name="index.html",
|
|
||||||
context={
|
|
||||||
"version": __version__,
|
|
||||||
"channels": channels_data,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/schedule/tag/{tag}", response_class=HTMLResponse)
|
@router.get("/schedule", response_class=HTMLResponse)
|
||||||
async def get_schedule_tag(request: Request, tag: str, live: bool = False):
|
async def get_schedule_list(request: AppRequest):
|
||||||
tag_value = TagUtil.parse_tag(tag)
|
schedule_api = request.app.state.api.schedule
|
||||||
schedule_api: ScheduleApi = request.app.state.schedule_api
|
channels = await schedule_api.get_channels()
|
||||||
channels = await schedule_api.get_channels()
|
channels_data = BUNDLE.select_items(channels)
|
||||||
responses = [
|
return templates.TemplateResponse(
|
||||||
await schedule_api.get_channel_schedule(channel, tag_value.date)
|
request=request,
|
||||||
for channel in channels
|
name="index.html",
|
||||||
]
|
context={
|
||||||
return templates.TemplateResponse(
|
"version": __version__,
|
||||||
request=request,
|
"channels": channels_data,
|
||||||
name="schedule.html",
|
},
|
||||||
context={
|
)
|
||||||
"version": __version__,
|
|
||||||
"tag_util": TagUtil,
|
|
||||||
"datetime": datetime,
|
|
||||||
"channels": channels,
|
|
||||||
"response": responses[0],
|
|
||||||
"responses": responses,
|
|
||||||
"live": live,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/schedule/{channel}", response_class=RedirectResponse)
|
|
||||||
async def get_channel_default(channel: str):
|
|
||||||
return RedirectResponse(f"{channel}/tag/today")
|
|
||||||
|
|
||||||
@app.get("/schedule/{channel}/tag/{tag}", response_class=HTMLResponse)
|
@router.get("/schedule/tag/{tag}", response_class=HTMLResponse)
|
||||||
async def get_channel_tag(request: Request, channel: str, tag: str):
|
async def get_schedule_tag(request: AppRequest, tag: str, live: bool = False):
|
||||||
tag_value = TagUtil.parse_tag(tag)
|
tag_value = TagUtil.parse_tag(tag)
|
||||||
schedule_api: ScheduleApi = request.app.state.schedule_api
|
schedule_api = request.app.state.api.schedule
|
||||||
if tag_value.type == TagType.DAY:
|
results = await schedule_api.get_all_schedules(tag_value.date)
|
||||||
response = await schedule_api.get_channel_schedule(channel, tag_value.date)
|
return templates.TemplateResponse(
|
||||||
else:
|
request=request,
|
||||||
raise ValueError(tag)
|
name="schedule.html",
|
||||||
return templates.TemplateResponse(
|
context={
|
||||||
request=request,
|
"version": __version__,
|
||||||
name="channel.html",
|
"tag_util": TagUtil,
|
||||||
context={
|
"datetime": datetime,
|
||||||
"version": __version__,
|
"response": results[0],
|
||||||
"tag_util": TagUtil,
|
"responses": results,
|
||||||
"datetime": datetime,
|
"live": live,
|
||||||
"response": response,
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.get("/schedule/{channel}", response_class=RedirectResponse)
|
||||||
|
async def get_channel_default(channel: str):
|
||||||
|
return RedirectResponse(f"{channel}/tag/today")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/schedule/{channel}/tag/{tag}", response_class=HTMLResponse)
|
||||||
|
async def get_channel_tag(request: AppRequest, channel: str, tag: str):
|
||||||
|
tag_value = TagUtil.parse_tag(tag)
|
||||||
|
schedule_api = request.app.state.api.schedule
|
||||||
|
if tag_value.type == TagType.DAY:
|
||||||
|
response = await schedule_api.get_channel_schedule(channel, tag_value.date)
|
||||||
|
else:
|
||||||
|
raise ValueError(tag)
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request,
|
||||||
|
name="channel.html",
|
||||||
|
context={
|
||||||
|
"version": __version__,
|
||||||
|
"tag_util": TagUtil,
|
||||||
|
"datetime": datetime,
|
||||||
|
"response": response,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,18 +0,0 @@
|
|||||||
tr {
|
|
||||||
border-bottom: 1px solid lightgray;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.live {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
font-style: italic;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
@@ -1,57 +1,40 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
{% block title %}
|
||||||
|
Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<head>
|
{% block header %}
|
||||||
<meta charset="UTF-8">
|
<span class="fs-4 text-body ms-2 me-2">/</span>
|
||||||
<meta name="viewport"
|
<app-link href="/schedule"
|
||||||
content="width=device-width, initial-scale=1.0">
|
icon="tv">{{_("TV program")}}</app-link>
|
||||||
<meta http-equiv="X-UA-Compatible"
|
{% endblock %}
|
||||||
content="ie=edge">
|
|
||||||
<title>Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</title>
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/common/style.css?v={{version}}">
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/schedule/style.css?v={{version}}">
|
|
||||||
<link rel="icon"
|
|
||||||
href="/static/schedule/favicon.ico?v={{version}}"
|
|
||||||
type="image/x-icon">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="app-container">
|
{% block content %}
|
||||||
<h3 class="app-header">
|
<h4>
|
||||||
<a class="app-link-home"
|
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
||||||
href="/">
|
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
||||||
<div></div>
|
<a class="button"
|
||||||
</a>
|
href="../..">⬆️</a>
|
||||||
<div class="app-title">
|
<span>{{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
||||||
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
<a class="button"
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
||||||
<a class="button"
|
</h4>
|
||||||
href="../..">⬆️</a>
|
<table class="table">
|
||||||
<span>{{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
<thead>
|
||||||
<a class="button"
|
<tr>
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
<td></td>
|
||||||
</div>
|
<td></td>
|
||||||
</h3>
|
<td></td>
|
||||||
|
</tr>
|
||||||
<table>
|
</thead>
|
||||||
<thead>
|
<tbody>
|
||||||
<tr>
|
{% for value in response.values %}
|
||||||
<td></td>
|
<tr class="{{'table-success' if value.live else ''}}">
|
||||||
<td></td>
|
<td>{{value.start.strftime('%H:%M')}}</td>
|
||||||
<td></td>
|
<td>{{(value.end - value.start) | timedelta_format}}</td>
|
||||||
</tr>
|
<td>{{value.label}}</td>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
{% endfor %}
|
||||||
{% for value in response.values %}
|
</tbody>
|
||||||
<tr class="{{'live' if value.live else ''}}">
|
</table>
|
||||||
<td>{{value.start.strftime('%H:%M')}}</td>
|
{% endblock %}
|
||||||
<td>{{(value.end - value.start) | timedelta_format}}</td>
|
|
||||||
<td>{{value.label}}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,38 +1,18 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
{% block title %}{{_("TV program")}}{% endblock %}
|
||||||
|
|
||||||
<head>
|
{% block content %}
|
||||||
<meta charset="UTF-8">
|
<h1>{{_("TV program")}}</h1>
|
||||||
<meta name="viewport"
|
<div class="list-group mb-5">
|
||||||
content="width=device-width, initial-scale=1.0">
|
<a href="schedule/tag/today"
|
||||||
<meta http-equiv="X-UA-Compatible"
|
class="list-group-item list-group-item-action px-4">
|
||||||
content="ie=edge">
|
<span class="fw-bold">Все</span>
|
||||||
<title>ТВ</title>
|
</a>
|
||||||
<link rel="stylesheet"
|
{% for channel in channels %}
|
||||||
href="/static/common/style.css?v={{version}}">
|
<a href="schedule/{{channel.id}}"
|
||||||
<link rel="stylesheet"
|
class="list-group-item list-group-item-action px-4">
|
||||||
href="/static/schedule/style.css?v={{version}}">
|
<span class="text-primary">{{channel.name}}</span>
|
||||||
<link rel="icon"
|
</a>
|
||||||
href="/static/schedule/favicon.ico?v={{version}}"
|
{% endfor %}
|
||||||
type="image/x-icon">
|
</div>
|
||||||
</head>
|
{% endblock %}
|
||||||
|
|
||||||
<body class="app-container">
|
|
||||||
<h3 class="app-header">
|
|
||||||
<a class="app-link-home"
|
|
||||||
href="/">
|
|
||||||
<div></div>
|
|
||||||
</a>
|
|
||||||
<div class="app-title">
|
|
||||||
<span>Телепрограмма</span>
|
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
<ul class="app-list">
|
|
||||||
<li style="margin-bottom: 0.25rem; font-weight: bold;"><a href="schedule/tag/today">Все</a></li>
|
|
||||||
{% for channel in channels %}
|
|
||||||
<li><a href="schedule/{{channel.id}}">{{channel.name}}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,69 +1,54 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
{% block title %}
|
||||||
|
{{'Прямые трансляции' if live else _("TV program")}} | {{response.date.strftime('%a, %d %B %Y')}}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<head>
|
{% block header %}
|
||||||
<meta charset="UTF-8">
|
<span class="fs-4 text-body ms-2 me-2">/</span>
|
||||||
<meta name="viewport"
|
<app-link href="/schedule"
|
||||||
content="width=device-width, initial-scale=1.0">
|
icon="tv">{{_("TV program")}}</app-link>
|
||||||
<meta http-equiv="X-UA-Compatible"
|
{% endblock %}
|
||||||
content="ie=edge">
|
|
||||||
<title>ТВ</title>
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/common/style.css?v={{version}}">
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/schedule/style.css?v={{version}}">
|
|
||||||
<link rel="icon"
|
|
||||||
href="/static/schedule/favicon.ico?v={{version}}"
|
|
||||||
type="image/x-icon">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="app-container">
|
{% block content %}
|
||||||
<h3 class="app-header">
|
<h4>
|
||||||
<a class="app-link-home"
|
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
||||||
href="/">
|
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
||||||
<div></div>
|
<a class="button"
|
||||||
</a>
|
href="..">⬆️</a>
|
||||||
<div class="app-title">
|
<span>{{'Прямые трансляции' if live else 'Программа'}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
||||||
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
<a class="button"
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
||||||
<a class="button"
|
</h4>
|
||||||
href="..">⬆️</a>
|
<div>
|
||||||
<span>{{'Прямые трансляции' if live else 'Программа'}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
<table class="table">
|
||||||
<a class="button"
|
<thead>
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
<tr>
|
||||||
</div>
|
<td></td>
|
||||||
</h3>
|
<td></td>
|
||||||
|
<td></td>
|
||||||
<table class="{{'live' if live else ''}}">
|
</tr>
|
||||||
<thead>
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
<td></td>
|
{% for response in responses %}
|
||||||
<td></td>
|
{% set values = (response.values|selectattr('live') if live else response.values)|list %}
|
||||||
<td></td>
|
{% if values|length > 0 %}
|
||||||
</tr>
|
<tr class="table-primary fs-4">
|
||||||
</thead>
|
<td colspan="3">
|
||||||
<tbody>
|
<div>{{response.channel.name}}</div>
|
||||||
{% for response in responses %}
|
</td>
|
||||||
{% set values = (response.values|selectattr('live') if live else response.values)|list %}
|
<td></td>
|
||||||
{% if values|length > 0 %}
|
<td></td>
|
||||||
<tr>
|
</tr>
|
||||||
<td colspan="3">
|
{% for value in values %}
|
||||||
<div class="title">{{response.channel.name}}</div>
|
<tr class="{{'table-success' if not live and value.live else ''}}">
|
||||||
</td>
|
<td>{{value.start.strftime('%H:%M')}}</td>
|
||||||
<td></td>
|
<td>{{(value.end - value.start) | timedelta_format}}</td>
|
||||||
<td></td>
|
<td>{{value.label}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% for value in values %}
|
{% endfor %}
|
||||||
<tr class="{{'live' if not live and value.live else ''}}">
|
{% endif %}
|
||||||
<td>{{value.start.strftime('%H:%M')}}</td>
|
{% endfor %}
|
||||||
<td>{{(value.end - value.start) | timedelta_format}}</td>
|
</tbody>
|
||||||
<td>{{value.label}}</td>
|
</table>
|
||||||
</tr>
|
</div>
|
||||||
{% endfor %}
|
{% endblock %}
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
34
gallery/easel/route/view/translation.py
Normal file
34
gallery/easel/route/view/translation.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import gettext
|
||||||
|
from contextvars import ContextVar
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import Cookie, Header, Request
|
||||||
|
|
||||||
|
_translation: ContextVar[gettext.GNUTranslations | gettext.NullTranslations] = (
|
||||||
|
ContextVar("translation")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def set_language(
|
||||||
|
request: Request,
|
||||||
|
accept_language: str = Header("en"),
|
||||||
|
language: str | None = Cookie(None),
|
||||||
|
):
|
||||||
|
# Simplify the header (e.g., "en-US,en;q=0.9" -> "en")
|
||||||
|
lang = language or accept_language.split(",")[0].split("-")[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
t = gettext.translation(
|
||||||
|
"messages", localedir=Path(__file__).parent / "locales", languages=[lang]
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
t = gettext.NullTranslations()
|
||||||
|
|
||||||
|
token = _translation.set(t)
|
||||||
|
request.state.language = lang
|
||||||
|
yield lang
|
||||||
|
_translation.reset(token)
|
||||||
|
|
||||||
|
|
||||||
|
def _(message: str) -> str:
|
||||||
|
return _translation.get().gettext(message)
|
||||||
@@ -1,88 +1,87 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import APIRouter, FastAPI
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from gallery.sketch.weather.api import WeatherApi
|
from gallery.easel.core import AppRequest
|
||||||
from gallery.sketch.weather.catalog import BUNDLE
|
|
||||||
from gallery.sketch.weather.mock import WEATHER_MOCK_DATA
|
|
||||||
from gallery.sketch.weather.model import WeatherResponse
|
from gallery.sketch.weather.model import WeatherResponse
|
||||||
from gallery.version import __version__
|
from gallery.version import __version__
|
||||||
|
|
||||||
from ..common.util import TagType, TagUtil
|
from ..common.util import TagType, TagUtil
|
||||||
|
from ..translation import _
|
||||||
from .filters import cloudness_icon, wind_direction_icon
|
from .filters import cloudness_icon, wind_direction_icon
|
||||||
|
|
||||||
|
|
||||||
def mount(app: FastAPI):
|
base_dir = Path(__file__).parent
|
||||||
base_dir = Path(__file__).parent
|
templates = Jinja2Templates(
|
||||||
app.mount("/static/weather", StaticFiles(directory=base_dir / "static"))
|
directory=[
|
||||||
templates = Jinja2Templates(directory=base_dir / "templates")
|
base_dir.parent / "common/templates",
|
||||||
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
base_dir / "templates",
|
||||||
templates.env.filters["cloudness_icon"] = cloudness_icon
|
]
|
||||||
|
)
|
||||||
|
templates.env.globals.update({"_": _})
|
||||||
|
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
||||||
|
templates.env.filters["cloudness_icon"] = cloudness_icon
|
||||||
|
|
||||||
def build_weather_response(request: Request, response: WeatherResponse):
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
request=request,
|
|
||||||
name="weather.html",
|
|
||||||
context={
|
|
||||||
"version": __version__,
|
|
||||||
"tag_util": TagUtil,
|
|
||||||
"datetime": datetime,
|
|
||||||
"response": response,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/weather", response_class=HTMLResponse)
|
def build_weather_response(request: AppRequest, response: WeatherResponse):
|
||||||
async def get_weather_list(request: Request):
|
return templates.TemplateResponse(
|
||||||
weather_api: WeatherApi = request.app.state.weather_api
|
request=request,
|
||||||
locations = await weather_api.get_locations()
|
name="weather.html",
|
||||||
locations_data = BUNDLE.select_items(locations)
|
context={
|
||||||
return templates.TemplateResponse(
|
"version": __version__,
|
||||||
request=request,
|
"tag_util": TagUtil,
|
||||||
name="index.html",
|
"datetime": datetime,
|
||||||
context={
|
"response": response,
|
||||||
"version": __version__,
|
},
|
||||||
"locations": locations_data,
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/weather/{location}", response_class=RedirectResponse)
|
router = APIRouter()
|
||||||
async def get_weather_default(location: str):
|
|
||||||
return RedirectResponse(f"{location}/tag/today")
|
|
||||||
|
|
||||||
@app.get("/weather/{location}/day/mock", response_class=HTMLResponse)
|
@router.get("/weather", response_class=HTMLResponse)
|
||||||
async def get_weather_day_mock(request: Request):
|
async def get_weather_index(request: AppRequest, query: str | None = None):
|
||||||
response = WEATHER_MOCK_DATA.get_response("day")
|
weather_api = request.app.state.api.weather
|
||||||
return build_weather_response(request, response)
|
locations = (await weather_api.find_locations(query)) if query else []
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request,
|
||||||
|
name="index.html",
|
||||||
|
context={
|
||||||
|
"version": __version__,
|
||||||
|
"locations": locations,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@app.get("/weather/{location}/days/mock", response_class=HTMLResponse)
|
|
||||||
async def get_weather_days_mock(request: Request):
|
|
||||||
response = WEATHER_MOCK_DATA.get_response("days")
|
|
||||||
return build_weather_response(request, response)
|
|
||||||
|
|
||||||
@app.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
|
@router.get("/weather/{location}", response_class=RedirectResponse)
|
||||||
async def get_weather_day(request: Request, location: str, date: datetime.date):
|
async def get_weather_default(location: str):
|
||||||
weather_api: WeatherApi = request.app.state.weather_api
|
return RedirectResponse(f"{location}/tag/today")
|
||||||
response = await weather_api.get_day(location, date)
|
|
||||||
return build_weather_response(request, response)
|
|
||||||
|
|
||||||
@app.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
|
|
||||||
async def get_weather_days(request: Request, location: str, days: int):
|
|
||||||
weather_api: WeatherApi = request.app.state.weather_api
|
|
||||||
response = await weather_api.get_days(location, days)
|
|
||||||
return build_weather_response(request, response)
|
|
||||||
|
|
||||||
@app.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse)
|
@router.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
|
||||||
async def get_weather_tag(request: Request, location: str, tag: str):
|
async def get_weather_day(request: AppRequest, location: str, date: datetime.date):
|
||||||
tag_value = TagUtil.parse_tag(tag)
|
weather_api = request.app.state.api.weather
|
||||||
weather_api: WeatherApi = request.app.state.weather_api
|
response = await weather_api.get_day(location, date)
|
||||||
if tag_value.type == TagType.DAY:
|
return build_weather_response(request, response)
|
||||||
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)
|
@router.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
|
||||||
else:
|
async def get_weather_days(request: AppRequest, location: str, days: int):
|
||||||
raise ValueError(tag)
|
weather_api = request.app.state.api.weather
|
||||||
return build_weather_response(request, response)
|
response = await weather_api.get_days(location, days)
|
||||||
|
return build_weather_response(request, response)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse)
|
||||||
|
async def get_weather_tag(request: AppRequest, location: str, tag: str):
|
||||||
|
tag_value = TagUtil.parse_tag(tag)
|
||||||
|
weather_api = request.app.state.api.weather
|
||||||
|
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)
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
from gallery.sketch.weather.model import Cloudness, Precipitation, Sky, WindDirection
|
from gallery.sketch.weather.model import (
|
||||||
|
Cloudness,
|
||||||
|
Precipitation,
|
||||||
|
Sky,
|
||||||
|
WindDirection,
|
||||||
|
WindDirectionDeg,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def wind_direction_icon(wind_direction: WindDirection) -> str:
|
def wind_direction_icon(wind_direction_deg: float) -> str:
|
||||||
|
wind_direction = WindDirectionDeg(wind_direction_deg).direction
|
||||||
return {
|
return {
|
||||||
WindDirection.N: "⬇️",
|
WindDirection.N: "⬇️",
|
||||||
WindDirection.NO: "↙️",
|
WindDirection.NE: "↙️",
|
||||||
WindDirection.O: "⬅️",
|
WindDirection.E: "⬅️",
|
||||||
WindDirection.SO: "↖️",
|
WindDirection.SE: "↖️",
|
||||||
WindDirection.S: "⬆️",
|
WindDirection.S: "⬆️",
|
||||||
WindDirection.SW: "↗️",
|
WindDirection.SW: "↗️",
|
||||||
WindDirection.W: "➡️",
|
WindDirection.W: "➡️",
|
||||||
@@ -31,6 +38,8 @@ def cloudness_icon(sky: Sky) -> list[str]:
|
|||||||
Cloudness.CLOUDY: "⛅",
|
Cloudness.CLOUDY: "⛅",
|
||||||
Cloudness.MAINLY_CLOUDY: "☁️",
|
Cloudness.MAINLY_CLOUDY: "☁️",
|
||||||
}[sky.cloudness]
|
}[sky.cloudness]
|
||||||
|
elif sky.precipitation in [Precipitation.SNOW, Precipitation.HEAVY_SNOW]:
|
||||||
|
main_icon = "🌨️"
|
||||||
else:
|
else:
|
||||||
main_icon = "🌧️"
|
main_icon = "🌧️"
|
||||||
icons = [main_icon]
|
icons = [main_icon]
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,72 +0,0 @@
|
|||||||
.header {
|
|
||||||
font-size: 1rem;
|
|
||||||
text-align: left;
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.date {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
background: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.date.now {
|
|
||||||
background: rgba(0, 128, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.date .value a {
|
|
||||||
all: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cloudness {
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cloudness .icon {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cloudness .icon:first-child {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temperature {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temperature .value {
|
|
||||||
padding: 0.1rem 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temperature .value.positive {
|
|
||||||
color: orangered;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temperature .value.negative {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wind .direction {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wind .gust {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.precipitation .value {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pressure {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pressure .value {
|
|
||||||
padding: 0.1rem 0.4rem;
|
|
||||||
color: blueviolet;
|
|
||||||
}
|
|
||||||
|
|
||||||
.humidity .value {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.5 KiB |
@@ -1,37 +1,73 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
{% block title %}Weather{% endblock %}
|
||||||
|
|
||||||
<head>
|
{% block content %}
|
||||||
<meta charset="UTF-8">
|
<h1>Weather</h1>
|
||||||
<meta name="viewport"
|
<form action=""
|
||||||
content="width=device-width, initial-scale=1.0">
|
method="get"
|
||||||
<meta http-equiv="X-UA-Compatible"
|
class="mb-4">
|
||||||
content="ie=edge">
|
<div class="input-group mb-3">
|
||||||
<title>Погода</title>
|
<input type="text"
|
||||||
<link rel="stylesheet"
|
class="form-control"
|
||||||
href="/static/common/style.css?v={{version}}">
|
id="query"
|
||||||
<link rel="stylesheet"
|
name="query"
|
||||||
href="/static/weather/style.css?v={{version}}">
|
placeholder="Enter the city name">
|
||||||
<link rel="icon"
|
<button class="btn btn-primary"
|
||||||
href="/static/weather/favicon.ico?v={{version}}"
|
type="submit">Search</button>
|
||||||
type="image/x-icon">
|
</div>
|
||||||
</head>
|
</form>
|
||||||
|
<ul id="locations"
|
||||||
|
class="list-group mb-5">
|
||||||
|
{% for location in locations %}
|
||||||
|
<a href="weather/{{location.id}}"
|
||||||
|
class="list-group-item list-group-item-action px-4"
|
||||||
|
onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});">
|
||||||
|
<span class="text-primary">{{location.name}}</span>
|
||||||
|
<span class="small ms-1 text-secondary">
|
||||||
|
{{location.country}}, {{location.district}}, {{location.subdistrict}}
|
||||||
|
</span>
|
||||||
|
<span></span>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
document.loadLocations = () => {
|
||||||
|
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
|
||||||
|
const container = document.querySelector('#locations');
|
||||||
|
container.innerHTML = '';
|
||||||
|
for (const [id, name] of Object.entries(locations)) {
|
||||||
|
const element = document.createElement('a');
|
||||||
|
element.href = `weather/${id}`;
|
||||||
|
element.className = 'list-group-item list-group-item-action px-4 d-flex justify-content-between align-items-start';
|
||||||
|
element.innerHTML = `
|
||||||
|
<span class="text-primary me-auto">${name}</span>
|
||||||
|
<span class="text-danger" onclick="removeLocation('${id}'); event.preventDefault();">✕</span>
|
||||||
|
`;
|
||||||
|
container.appendChild(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<body class="app-container">
|
document.saveLocation = (location) => {
|
||||||
<h3 class="app-header">
|
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
|
||||||
<a class="app-link-home"
|
locations[location.id] = location.name;
|
||||||
href="/">
|
window.localStorage.setItem('locations', JSON.stringify(locations));
|
||||||
<div></div>
|
}
|
||||||
</a>
|
|
||||||
<div class="app-title">
|
|
||||||
<span>Погода</span>
|
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
<ul class="app-list">
|
|
||||||
{% for location in locations %}
|
|
||||||
<li><a href="weather/{{location.id}}">{{location.name}}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
document.removeLocation = (id) => {
|
||||||
|
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
|
||||||
|
delete locations[id];
|
||||||
|
window.localStorage.setItem('locations', JSON.stringify(locations));
|
||||||
|
document.loadLocations();
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const searchQuery = params.get('query');
|
||||||
|
if (searchQuery) {
|
||||||
|
document.querySelector('#query').value = searchQuery;
|
||||||
|
} else {
|
||||||
|
document.loadLocations();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,184 +1,168 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
{% block title %}{{_("Weather")}} | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}{% endblock %}
|
||||||
|
|
||||||
<head>
|
{% block header %}
|
||||||
<meta charset="UTF-8">
|
<span class="fs-4 text-body ms-2 me-2">/</span>
|
||||||
<meta name="viewport"
|
<app-link href="/weather" icon="brightness-high">{{_("Weather")}}</app-link>
|
||||||
content="width=device-width, initial-scale=1.0">
|
{% endblock %}
|
||||||
<meta http-equiv="X-UA-Compatible"
|
|
||||||
content="ie=edge">
|
|
||||||
<title>Погода | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</title>
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/common/style.css?v={{version}}">
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/weather/style.css?v={{version}}">
|
|
||||||
<link rel="icon"
|
|
||||||
href="/static/weather/favicon.ico?v={{version}}"
|
|
||||||
type="image/x-icon">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="app-container">
|
{% block content %}
|
||||||
<h3 class="app-header">
|
<h4>
|
||||||
<a class="app-link-home"
|
{% if response.period == 'day' %}
|
||||||
href="/">
|
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
||||||
<div></div>
|
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
||||||
</a>
|
<a class="button"
|
||||||
<div class="app-title">
|
href="../tag/days-10">⬆️</a>
|
||||||
{% if response.period == 'day' %}
|
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
||||||
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
<a class="button"
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
||||||
<a class="button"
|
{% endif %}
|
||||||
href="../tag/days-10">⬆️</a>
|
{% if response.period == 'days' %}
|
||||||
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
||||||
<a class="button"
|
{% endif %}
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
</h4>
|
||||||
{% endif %}
|
<div class="table-responsive">
|
||||||
{% if response.period == 'days' %}
|
<table class="table table-weather table-borderless table-compact text-center w-auto"
|
||||||
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
style="font-size: 130%;">
|
||||||
{% endif %}
|
<tbody>
|
||||||
</div>
|
<!-- date -->
|
||||||
</h3>
|
<tr>
|
||||||
<table>
|
{% for value in response.values %}
|
||||||
<tbody>
|
{% if response.period == 'day' %}
|
||||||
<!-- date -->
|
<td
|
||||||
<tr>
|
class="date {{'now' if value.date < datetime.datetime.now() and value.date + datetime.timedelta(hours=3) > datetime.datetime.now() else ''}}">
|
||||||
{% for value in response.values %}
|
<span class="value">{{value.date.strftime('%H:%M')}}</span>
|
||||||
{% if response.period == 'day' %}
|
</td>
|
||||||
<td
|
{% endif %}
|
||||||
class="date {{'now' if value.date < datetime.datetime.now() and value.date + datetime.timedelta(hours=3) > datetime.datetime.now() else ''}}">
|
{% if response.period == 'days' %}
|
||||||
<span class="value">{{value.date.strftime('%H:%M')}}</span>
|
<td class="date {{'now' if value.date.date() == datetime.date.today() else ''}}">
|
||||||
</td>
|
<span class="value">
|
||||||
{% endif %}
|
<a href="../tag/{{tag_util.create_tag('day', value.date.date())}}">
|
||||||
{% if response.period == 'days' %}
|
{{value.date.strftime('%a %d')}}
|
||||||
<td class="date {{'now' if value.date.date() == datetime.date.today() else ''}}">
|
</a>
|
||||||
<span class="value">
|
</span>
|
||||||
<a href="../tag/{{tag_util.create_tag('day', value.date.date())}}">
|
</td>
|
||||||
{{value.date.strftime('%a %d')}}
|
{% endif %}
|
||||||
</a>
|
{% endfor %}
|
||||||
</span>
|
</tr>
|
||||||
</td>
|
<!-- cloudness -->
|
||||||
{% endif %}
|
<tr>
|
||||||
{% endfor %}
|
<td colspan="{{response.values | length}}"
|
||||||
</tr>
|
class="header">
|
||||||
<!-- cloudness -->
|
Облачность
|
||||||
<tr>
|
</td>
|
||||||
<td colspan="{{response.values | length}}"
|
</tr>
|
||||||
class="header">
|
<tr>
|
||||||
Облачность
|
{% for value in response.values %}
|
||||||
</td>
|
<td class="cloudness">
|
||||||
</tr>
|
{% for icon in value.sky | cloudness_icon %}
|
||||||
<tr>
|
<div class="icon">{{icon}}</div>
|
||||||
{% for value in response.values %}
|
{% endfor %}
|
||||||
<td class="cloudness">
|
</td>
|
||||||
{% for icon in value.sky | cloudness_icon %}
|
{% endfor %}
|
||||||
<div class="icon">{{icon}}</div>
|
</tr>
|
||||||
{% endfor %}
|
<!-- temperature -->
|
||||||
</td>
|
<tr>
|
||||||
{% endfor %}
|
<td colspan="{{response.values | length}}"
|
||||||
</tr>
|
class="header">
|
||||||
<!-- temperature -->
|
Температура, °C
|
||||||
<tr>
|
</td>
|
||||||
<td colspan="{{response.values | length}}"
|
</tr>
|
||||||
class="header">
|
<tr>
|
||||||
Температура, °C
|
{% for value in response.values %}
|
||||||
</td>
|
<td class="temperature">
|
||||||
</tr>
|
{% for temperature in value.temperature %}
|
||||||
<tr>
|
<div class="value {{'positive' if temperature > 0 else 'negative'}}"
|
||||||
{% for value in response.values %}
|
style="background-color: rgba(255, 128, 128, {{(temperature - 10) * 0.015}});">
|
||||||
<td class="temperature">
|
{{temperature}}
|
||||||
{% for temperature in value.temperature %}
|
</div>
|
||||||
<div class="value {{'positive' if temperature > 0 else 'negative'}}"
|
{% endfor %}
|
||||||
style="background-color: rgba(255, 128, 128, {{(temperature - 10) * 0.015}});">
|
</td>
|
||||||
{{temperature}}
|
{% endfor %}
|
||||||
</div>
|
</tr>
|
||||||
{% endfor %}
|
<!-- wind_direction -->
|
||||||
</td>
|
<tr>
|
||||||
{% endfor %}
|
<td colspan="{{response.values | length}}"
|
||||||
</tr>
|
class="header">
|
||||||
<!-- wind_direction -->
|
Направление ветра
|
||||||
<tr>
|
</td>
|
||||||
<td colspan="{{response.values | length}}"
|
</tr>
|
||||||
class="header">
|
<tr>
|
||||||
Направление ветра
|
{% for value in response.values %}
|
||||||
</td>
|
<td class="wind">
|
||||||
</tr>
|
<span class="icon">{{value.wind_direction | wind_direction_icon}}</span>
|
||||||
<tr>
|
</td>
|
||||||
{% for value in response.values %}
|
{% endfor %}
|
||||||
<td class="wind">
|
</tr>
|
||||||
<span class="icon">{{value.wind_direction | wind_direction_icon}}</span>
|
<!-- wind_speed -->
|
||||||
</td>
|
<tr>
|
||||||
{% endfor %}
|
<td colspan="{{response.values | length}}"
|
||||||
</tr>
|
class="header">
|
||||||
<!-- wind_speed -->
|
Скорость ветра, м/с
|
||||||
<tr>
|
</td>
|
||||||
<td colspan="{{response.values | length}}"
|
</tr>
|
||||||
class="header">
|
<tr>
|
||||||
Скорость ветра, м/с
|
{% for value in response.values %}
|
||||||
</td>
|
<td class="wind"
|
||||||
</tr>
|
style="background-color: rgba(128, 128, 128, {{value.wind_speed * 0.05}});">
|
||||||
<tr>
|
<span class="speed">{{value.wind_speed}}</span>
|
||||||
{% for value in response.values %}
|
{% if value.wind_gust != value.wind_speed %}
|
||||||
<td class="wind"
|
<span class="gust">
|
||||||
style="background-color: rgba(128, 128, 128, {{value.wind_speed * 0.05}});">
|
({{value.wind_gust}})
|
||||||
<span class="speed">{{value.wind_speed}}</span>
|
</span>
|
||||||
{% if value.wind_gust != value.wind_speed %}
|
{% endif %}
|
||||||
<span class="gust">
|
</td>
|
||||||
({{value.wind_gust}})
|
{% endfor %}
|
||||||
</span>
|
</tr>
|
||||||
{% endif %}
|
<!-- precipitation -->
|
||||||
</td>
|
<tr>
|
||||||
{% endfor %}
|
<td colspan="{{response.values | length}}"
|
||||||
</tr>
|
class="header">
|
||||||
<!-- precipitation -->
|
Осадки, мм
|
||||||
<tr>
|
</td>
|
||||||
<td colspan="{{response.values | length}}"
|
</tr>
|
||||||
class="header">
|
<tr>
|
||||||
Осадки, мм
|
{% for value in response.values %}
|
||||||
</td>
|
<td class="precipitation"
|
||||||
</tr>
|
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
|
||||||
<tr>
|
<span class="value">{{value.precipitation or ' '}}</span>
|
||||||
{% for value in response.values %}
|
</td>
|
||||||
<td class="precipitation"
|
{% endfor %}
|
||||||
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
|
</tr>
|
||||||
<span class="value">{{value.precipitation or ' '}}</span>
|
<!-- pressure -->
|
||||||
</td>
|
<tr>
|
||||||
{% endfor %}
|
<td colspan="{{response.values | length}}"
|
||||||
</tr>
|
class="header">
|
||||||
<!-- pressure -->
|
Давление, мм рт. ст.
|
||||||
<tr>
|
</td>
|
||||||
<td colspan="{{response.values | length}}"
|
</tr>
|
||||||
class="header">
|
<tr>
|
||||||
Давление, мм рт. ст.
|
{% for value in response.values %}
|
||||||
</td>
|
<td class="pressure">
|
||||||
</tr>
|
{% for pressure in value.pressure %}
|
||||||
<tr>
|
<div class="value"
|
||||||
{% for value in response.values %}
|
style="background-color: rgba(128, 0, 255, {{(pressure - 720) * 0.008}});">
|
||||||
<td class="pressure">
|
{{pressure}}</div>
|
||||||
{% for pressure in value.pressure %}
|
{% endfor %}
|
||||||
<div class="value"
|
</td>
|
||||||
style="background-color: rgba(128, 0, 255, {{(pressure - 720) * 0.008}});">
|
{% endfor %}
|
||||||
{{pressure}}</div>
|
</tr>
|
||||||
{% endfor %}
|
<!-- humidity -->
|
||||||
</td>
|
<tr>
|
||||||
{% endfor %}
|
<td colspan="{{response.values | length}}"
|
||||||
</tr>
|
class="header">
|
||||||
<!-- humidity -->
|
Влажность, %
|
||||||
<tr>
|
</td>
|
||||||
<td colspan="{{response.values | length}}"
|
</tr>
|
||||||
class="header">
|
<tr>
|
||||||
Влажность, %
|
{% for value in response.values %}
|
||||||
</td>
|
<td class="humidity"
|
||||||
</tr>
|
style="background-color: rgba(128, 128, 255, {{value.humidity * 0.005}});">
|
||||||
<tr>
|
<span class="value">{{value.humidity}}</span>
|
||||||
{% for value in response.values %}
|
</td>
|
||||||
<td class="humidity"
|
{% endfor %}
|
||||||
style="background-color: rgba(128, 128, 255, {{value.humidity * 0.005}});">
|
</tr>
|
||||||
<span class="value">{{value.humidity}}</span>
|
</tbody>
|
||||||
</td>
|
</table>
|
||||||
{% endfor %}
|
</div>
|
||||||
</tr>
|
{% endblock %}
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -6,19 +6,28 @@ import uvicorn
|
|||||||
from gallery.easel import build_app
|
from gallery.easel import build_app
|
||||||
from gallery.painting.gismeteo.api import GismeteoApi
|
from gallery.painting.gismeteo.api import GismeteoApi
|
||||||
from gallery.painting.matchtv.api import MatchTvApi
|
from gallery.painting.matchtv.api import MatchTvApi
|
||||||
|
from gallery.painting.openweather.api import OpenWeatherApi
|
||||||
|
from gallery.painting.yandextv.api import YandexTvApi
|
||||||
|
from gallery.sketch.bundle import ApiBundle
|
||||||
from gallery.sketch.schedule.cached import CachedScheduleApi
|
from gallery.sketch.schedule.cached import CachedScheduleApi
|
||||||
from gallery.sketch.weather.cached import CachedWeatherApi
|
from gallery.sketch.weather.cached import CachedWeatherApi
|
||||||
|
|
||||||
weather_api = CachedWeatherApi(GismeteoApi())
|
api = ApiBundle(
|
||||||
schedule_api = CachedScheduleApi(MatchTvApi())
|
[
|
||||||
app = build_app(weather_api, schedule_api)
|
CachedScheduleApi(YandexTvApi()),
|
||||||
|
CachedScheduleApi(MatchTvApi()),
|
||||||
|
CachedWeatherApi(GismeteoApi()),
|
||||||
|
CachedWeatherApi(OpenWeatherApi()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
app = build_app(api)
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"gallery.main:app",
|
"gallery.main:app",
|
||||||
host="0.0.0.0",
|
host=environ.get("GALLERY_HOST", "0.0.0.0"),
|
||||||
port=8000,
|
port=int(environ.get("GALLERY_PORT", 8000)),
|
||||||
log_config=str(Path(__file__).parent / "logging.yaml"),
|
log_config=str(Path(__file__).parent / "logging.yaml"),
|
||||||
reload="DEBUG" in environ,
|
reload="DEBUG" in environ,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List
|
from typing import Any
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from gallery.sketch.source import ApiSource
|
from gallery.sketch.source import ApiSource
|
||||||
from gallery.sketch.weather.api import WeatherApi
|
from gallery.sketch.weather.api import WeatherApi
|
||||||
from gallery.sketch.weather.catalog import LocationId
|
from gallery.sketch.weather.model import Location, WeatherResponse, WeatherValue
|
||||||
from gallery.sketch.weather.model import WeatherResponse, WeatherValue
|
|
||||||
|
|
||||||
from . import datehelp
|
from . import datehelp
|
||||||
from .parser import DAYS_PARSER, LOCATION_PARSER, ONE_DAY_PARSER, ROW_PARSERS
|
from .parser import DAYS_PARSER, LOCATION_PARSER, ONE_DAY_PARSER, ROW_PARSERS
|
||||||
@@ -34,7 +34,7 @@ class GismeteoApi(WeatherApi):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _parse_oneday(self, date: datetime.date, data: str) -> WeatherResponse:
|
def _parse_oneday(self, date: datetime.date, data: str) -> WeatherResponse:
|
||||||
result: List[Dict[str, Any]] = []
|
result: list[dict[str, Any]] = []
|
||||||
soup = BeautifulSoup(data, features="html.parser")
|
soup = BeautifulSoup(data, features="html.parser")
|
||||||
location = LOCATION_PARSER.parse_location(data)
|
location = LOCATION_PARSER.parse_location(data)
|
||||||
widget = ONE_DAY_PARSER.parse_widget(soup)
|
widget = ONE_DAY_PARSER.parse_widget(soup)
|
||||||
@@ -52,7 +52,7 @@ class GismeteoApi(WeatherApi):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _parse_manydays(self, data: str) -> WeatherResponse:
|
def _parse_manydays(self, data: str) -> WeatherResponse:
|
||||||
result: List[Dict[str, Any]] = []
|
result: list[dict[str, Any]] = []
|
||||||
soup = BeautifulSoup(data, features="html.parser")
|
soup = BeautifulSoup(data, features="html.parser")
|
||||||
location = LOCATION_PARSER.parse_location(data)
|
location = LOCATION_PARSER.parse_location(data)
|
||||||
widget = DAYS_PARSER.parse_widget(soup)
|
widget = DAYS_PARSER.parse_widget(soup)
|
||||||
@@ -69,11 +69,33 @@ class GismeteoApi(WeatherApi):
|
|||||||
values=values,
|
values=values,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_locations(self) -> list[str]:
|
async def find_locations(self, query: str) -> list[Location]:
|
||||||
return [
|
geo = "ru"
|
||||||
LocationId.OREL,
|
latitude = 52.968498
|
||||||
LocationId.ZMIYEVKA,
|
longitude = 36.0695
|
||||||
]
|
data = json.loads(
|
||||||
|
await self.SOURCE.request(
|
||||||
|
f"mq/city/q/?q={query}&geo={geo}&latitude={latitude}&longitude={longitude}&limit=10"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = []
|
||||||
|
for item in data["data"]:
|
||||||
|
result.append(
|
||||||
|
Location(
|
||||||
|
id=f"{item['slug']}-{item['id']}",
|
||||||
|
name=item["translations"]["kk"]["city"]["name"],
|
||||||
|
lat=item["coordinates"]["latitude"],
|
||||||
|
lon=item["coordinates"]["longitude"],
|
||||||
|
country=item["translations"]["kk"]["country"]["name"],
|
||||||
|
district=item["translations"]["kk"]["district"]["name"],
|
||||||
|
subdistrict=(
|
||||||
|
item["translations"]["kk"]["subdistrict"]["name"]
|
||||||
|
if "subdistrict" in item["translations"]["kk"]
|
||||||
|
else ""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
||||||
data = await self.SOURCE.request(f"weather-{location_id}/{datehelp.dump(date)}")
|
data = await self.SOURCE.request(f"weather-{location_id}/{datehelp.dump(date)}")
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,7 +5,13 @@ from typing import Iterable
|
|||||||
import dateparser
|
import dateparser
|
||||||
from bs4 import Tag
|
from bs4 import Tag
|
||||||
|
|
||||||
from gallery.sketch.weather.model import Cloudness, Precipitation, Sky, WindDirection
|
from gallery.sketch.weather.model import (
|
||||||
|
Cloudness,
|
||||||
|
Precipitation,
|
||||||
|
Sky,
|
||||||
|
WindDirection,
|
||||||
|
WindDirectionDeg,
|
||||||
|
)
|
||||||
|
|
||||||
from .core import BaseWidgetParser, RowParser
|
from .core import BaseWidgetParser, RowParser
|
||||||
|
|
||||||
@@ -61,8 +67,17 @@ class SkyParser(RowParser[Sky]):
|
|||||||
PRECIPITATION_MAP: dict[str, Precipitation] = {
|
PRECIPITATION_MAP: dict[str, Precipitation] = {
|
||||||
"без осадков": Precipitation.NO,
|
"без осадков": Precipitation.NO,
|
||||||
"небольшой дождь": Precipitation.SMALL_RAIN,
|
"небольшой дождь": Precipitation.SMALL_RAIN,
|
||||||
|
"сильный дождь": Precipitation.HEAVY_RAIN,
|
||||||
"дождь": Precipitation.RAIN,
|
"дождь": Precipitation.RAIN,
|
||||||
"ливень": Precipitation.SHOWER,
|
"ливень": Precipitation.SHOWER,
|
||||||
|
"снег": Precipitation.SNOW,
|
||||||
|
"небольшой снег": Precipitation.SNOW,
|
||||||
|
"сильный снег": Precipitation.HEAVY_SNOW,
|
||||||
|
"мокрый снег": Precipitation.SNOW,
|
||||||
|
"снег с дождём": Precipitation.SNOW,
|
||||||
|
"сильный снег с дождём": Precipitation.HEAVY_SNOW,
|
||||||
|
"небольшой снег с дождём": Precipitation.SNOW,
|
||||||
|
"небольшой мокрый снег": Precipitation.SNOW,
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[Sky]:
|
def parse_row(self, tag: Tag) -> Iterable[Sky]:
|
||||||
@@ -106,7 +121,7 @@ class WindSpeedParser(RowParser[int]):
|
|||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[int]:
|
def parse_row(self, tag: Tag) -> Iterable[int]:
|
||||||
for item in tag.select(
|
for item in tag.select(
|
||||||
".widget-row[data-row=wind-speed] > .row-item > speed-value"
|
".widget-row-wind > .row-item > .wind-speed > speed-value"
|
||||||
):
|
):
|
||||||
yield int(item.attrs["value"])
|
yield int(item.attrs["value"])
|
||||||
|
|
||||||
@@ -115,7 +130,7 @@ class WindGustParser(RowParser[int]):
|
|||||||
KEY = "wind_gust"
|
KEY = "wind_gust"
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[int]:
|
def parse_row(self, tag: Tag) -> Iterable[int]:
|
||||||
for item in tag.select(".widget-row[data-row=wind-gust] > .row-item"):
|
for item in tag.select(".widget-row-wind > .row-item > .wind-gust"):
|
||||||
value = item.select_one("speed-value")
|
value = item.select_one("speed-value")
|
||||||
yield int(value.attrs["value"]) if value else 0
|
yield int(value.attrs["value"]) if value else 0
|
||||||
|
|
||||||
@@ -124,26 +139,29 @@ class WindDirectionParser(RowParser[WindDirection]):
|
|||||||
KEY = "wind_direction"
|
KEY = "wind_direction"
|
||||||
|
|
||||||
WIND_DIRECTION_MAP: dict[str, WindDirection] = {
|
WIND_DIRECTION_MAP: dict[str, WindDirection] = {
|
||||||
|
"—": WindDirection.CALM,
|
||||||
"штиль": WindDirection.CALM,
|
"штиль": WindDirection.CALM,
|
||||||
"с": WindDirection.N,
|
"с": WindDirection.N,
|
||||||
"св": WindDirection.NO,
|
"св": WindDirection.NE,
|
||||||
"в": WindDirection.O,
|
"в": WindDirection.E,
|
||||||
"юв": WindDirection.SO,
|
"юв": WindDirection.SE,
|
||||||
"ю": WindDirection.S,
|
"ю": WindDirection.S,
|
||||||
"юз": WindDirection.SW,
|
"юз": WindDirection.SW,
|
||||||
"з": WindDirection.W,
|
"з": WindDirection.W,
|
||||||
"сз": WindDirection.NW,
|
"сз": WindDirection.NW,
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[WindDirection]:
|
def parse_row(self, tag: Tag) -> Iterable[float]:
|
||||||
for item in tag.select(
|
for item in tag.select(
|
||||||
".widget-row[data-row=wind-direction] > .row-item > .direction"
|
".widget-row-wind > .row-item > .wind-speed > .wind-direction"
|
||||||
):
|
):
|
||||||
wind_direction_str = item.text.lower()
|
wind_direction_str = item.text.lower().strip()
|
||||||
yield self.WIND_DIRECTION_MAP[wind_direction_str]
|
yield WindDirectionDeg.from_direction(
|
||||||
|
self.WIND_DIRECTION_MAP[wind_direction_str]
|
||||||
|
).value
|
||||||
|
|
||||||
|
|
||||||
class WindPrecipitationParser(RowParser[float]):
|
class PrecipitationParser(RowParser[float]):
|
||||||
KEY = "precipitation"
|
KEY = "precipitation"
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[float]:
|
def parse_row(self, tag: Tag) -> Iterable[float]:
|
||||||
@@ -167,7 +185,9 @@ class HumidityParser(RowParser[int]):
|
|||||||
KEY = "humidity"
|
KEY = "humidity"
|
||||||
|
|
||||||
def parse_row(self, tag: Tag) -> Iterable[int]:
|
def parse_row(self, tag: Tag) -> Iterable[int]:
|
||||||
for item in tag.select(".widget-row[data-row=humidity] > .row-item"):
|
for item in tag.select(
|
||||||
|
".widget-row[data-row=humidity] > .row-item, .widget-row[data-row=humidity-avg] > .row-item"
|
||||||
|
):
|
||||||
yield int(item.text)
|
yield int(item.text)
|
||||||
|
|
||||||
|
|
||||||
@@ -178,7 +198,7 @@ ROW_PARSERS: list[RowParser] = [
|
|||||||
WindSpeedParser(),
|
WindSpeedParser(),
|
||||||
WindGustParser(),
|
WindGustParser(),
|
||||||
WindDirectionParser(),
|
WindDirectionParser(),
|
||||||
WindPrecipitationParser(),
|
PrecipitationParser(),
|
||||||
PressureParser(),
|
PressureParser(),
|
||||||
HumidityParser(),
|
HumidityParser(),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import logging
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from gallery.sketch.schedule.api import ScheduleApi
|
from gallery.sketch.schedule.api import ScheduleApi
|
||||||
from gallery.sketch.schedule.catalog import ChannelId
|
from gallery.sketch.schedule.model import Channel, ChannelId, Schedule, ScheduleValue
|
||||||
from gallery.sketch.schedule.model import Channel, Schedule, ScheduleValue
|
|
||||||
from gallery.sketch.source import ApiSource
|
from gallery.sketch.source import ApiSource
|
||||||
|
|
||||||
logger = logging.getLogger("matchtv")
|
logger = logging.getLogger("matchtv")
|
||||||
@@ -15,7 +14,7 @@ class MatchTvApi(ScheduleApi):
|
|||||||
PROVIDER = "matchtv"
|
PROVIDER = "matchtv"
|
||||||
SOURCE = ApiSource("https://matchtv.ru")
|
SOURCE = ApiSource("https://matchtv.ru")
|
||||||
|
|
||||||
async def get_channels(self) -> list[str]:
|
async def get_channels(self) -> list[ChannelId]:
|
||||||
return [
|
return [
|
||||||
ChannelId.MATCH_TV,
|
ChannelId.MATCH_TV,
|
||||||
ChannelId.MATCH_IGRA,
|
ChannelId.MATCH_IGRA,
|
||||||
@@ -27,21 +26,31 @@ class MatchTvApi(ScheduleApi):
|
|||||||
]
|
]
|
||||||
|
|
||||||
async def get_channel_schedule(
|
async def get_channel_schedule(
|
||||||
self, channel_id: str, date: datetime.date
|
self, channel_id: ChannelId, date: datetime.date
|
||||||
) -> Schedule:
|
) -> Schedule:
|
||||||
endpoint = f"channel/{channel_id}/tvguide?date={date:%d-%m-%Y}"
|
endpoint = f"tvguide/{channel_id}?date={date:%Y%m%d}"
|
||||||
data = await self.SOURCE.request(endpoint)
|
data = await self.SOURCE.request(endpoint)
|
||||||
soup = BeautifulSoup(data, features="html.parser")
|
soup = BeautifulSoup(data, features="html.parser")
|
||||||
values = []
|
values = []
|
||||||
channel_name = soup.select_one(".caption__heading").text.split("|")[0].strip()
|
channel_name = (
|
||||||
|
soup.select_one(".p-tv-guide-header__title")
|
||||||
|
.text.replace("Телепрограмма ", "")
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
current_day = datetime.datetime.combine(
|
current_day = datetime.datetime.combine(
|
||||||
date.today(), datetime.datetime.min.time()
|
date.today(), datetime.datetime.min.time()
|
||||||
)
|
)
|
||||||
end = current_day + datetime.timedelta(days=1, hours=6)
|
end = current_day + datetime.timedelta(days=1, hours=6)
|
||||||
prev_value: ScheduleValue | None = None
|
prev_value: ScheduleValue | None = None
|
||||||
for item in soup.select(".teleprogram-schedule .teleprogram-schedule__item"):
|
for item in soup.select(
|
||||||
title = item.select_one(".teleprogram-item__title").text.strip()
|
".p-tv-guide-schedule-channel-carcass__transmissions .p-tv-guide-schedule-channel-transmission"
|
||||||
time_str = item.select_one(".teleprogram-item__time").text.strip()
|
):
|
||||||
|
title = item.select_one(
|
||||||
|
".p-tv-guide-schedule-channel-transmission__title"
|
||||||
|
).text.strip()
|
||||||
|
time_str = item.select_one(
|
||||||
|
".p-tv-guide-schedule-channel-transmission__time-block"
|
||||||
|
).text.strip()
|
||||||
hours, minutes = map(int, time_str.split(":"))
|
hours, minutes = map(int, time_str.split(":"))
|
||||||
item_date = current_day.replace(hour=hours, minute=minutes)
|
item_date = current_day.replace(hour=hours, minute=minutes)
|
||||||
if prev_value is not None and item_date.hour < prev_value.start.hour:
|
if prev_value is not None and item_date.hour < prev_value.start.hour:
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from gallery.sketch.mock import MockData
|
|
||||||
|
|
||||||
MATCHTV_MOCK_DATA = MockData(Path(__file__).parent / "data")
|
|
||||||
File diff suppressed because one or more lines are too long
0
gallery/painting/openweather/__init__.py
Normal file
0
gallery/painting/openweather/__init__.py
Normal file
67
gallery/painting/openweather/api.py
Normal file
67
gallery/painting/openweather/api.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from aiocache import cached
|
||||||
|
|
||||||
|
from gallery.sketch.weather.api import WeatherApi
|
||||||
|
from gallery.sketch.weather.model import Location, WeatherResponse, WeatherValue
|
||||||
|
from gallery.sketch.weather.util import merge_weather_values
|
||||||
|
from gallery.util import TimeUnit
|
||||||
|
|
||||||
|
from .openweather import Forecast, OpenWeather
|
||||||
|
from .parser import FORECAST_ITEM_PARSER
|
||||||
|
|
||||||
|
logger = logging.getLogger("openweather")
|
||||||
|
|
||||||
|
|
||||||
|
class OpenWeatherApi(WeatherApi):
|
||||||
|
PROVIDER = "openweather"
|
||||||
|
SOURCE = OpenWeather("517a6bccceaa1c48127f6199ec3fb7cf")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_location(cls, location_id: str) -> tuple[float, float]:
|
||||||
|
return tuple(map(float, location_id.split(":", maxsplit=2)))
|
||||||
|
|
||||||
|
@cached(
|
||||||
|
key_builder=lambda fun, self, location_id: f"api.weather.{self.provider}.source.{location_id}.forecast",
|
||||||
|
alias="redis",
|
||||||
|
ttl=TimeUnit.DAY,
|
||||||
|
)
|
||||||
|
async def _get_location_forecast(self, location_id: str) -> Forecast:
|
||||||
|
return await self.SOURCE.get_forecast(*self._parse_location(location_id))
|
||||||
|
|
||||||
|
async def find_locations(self, query: str) -> list[Location]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
||||||
|
data: Forecast = await self._get_location_forecast(location_id)
|
||||||
|
values = []
|
||||||
|
for item in data.list:
|
||||||
|
value = FORECAST_ITEM_PARSER.parse(item)
|
||||||
|
if value.date.date() == date:
|
||||||
|
values.append(value)
|
||||||
|
return WeatherResponse(
|
||||||
|
location=location_id,
|
||||||
|
date=date,
|
||||||
|
period="day",
|
||||||
|
values=values,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_days(self, location_id: str, days: int) -> WeatherResponse:
|
||||||
|
data: Forecast = await self._get_location_forecast(location_id)
|
||||||
|
values_by_date: dict[datetime.datetime, list[WeatherValue]] = defaultdict(list)
|
||||||
|
for item in data.list:
|
||||||
|
value = FORECAST_ITEM_PARSER.parse(item)
|
||||||
|
item_date = value.date.replace(hour=0, minute=0)
|
||||||
|
values_by_date[item_date].append(value)
|
||||||
|
values = [
|
||||||
|
merge_weather_values(date, values)
|
||||||
|
for date, values in values_by_date.items()
|
||||||
|
]
|
||||||
|
return WeatherResponse(
|
||||||
|
location=location_id,
|
||||||
|
date=datetime.date.today(),
|
||||||
|
period="days",
|
||||||
|
values=list(sorted(values, key=lambda item: item.date)),
|
||||||
|
)
|
||||||
@@ -2,4 +2,4 @@ from pathlib import Path
|
|||||||
|
|
||||||
from gallery.sketch.mock import MockData
|
from gallery.sketch.mock import MockData
|
||||||
|
|
||||||
GISMETEO_MOCK_DATA = MockData(Path(__file__).parent / "data")
|
OPENWEATHER_MOCK_DATA = MockData(Path(__file__).parent / "data")
|
||||||
83
gallery/painting/openweather/openweather.py
Normal file
83
gallery/painting/openweather/openweather.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from gallery.sketch.source import ApiSource
|
||||||
|
|
||||||
|
|
||||||
|
class Model(BaseModel):
|
||||||
|
class Config:
|
||||||
|
use_enum_values = True
|
||||||
|
|
||||||
|
|
||||||
|
class Main(Model):
|
||||||
|
temp: float
|
||||||
|
feels_like: float
|
||||||
|
temp_min: float
|
||||||
|
temp_max: float
|
||||||
|
pressure: int
|
||||||
|
sea_level: int
|
||||||
|
grnd_level: int
|
||||||
|
humidity: int
|
||||||
|
temp_kf: float
|
||||||
|
|
||||||
|
|
||||||
|
class Weather(Model):
|
||||||
|
id: int
|
||||||
|
main: str
|
||||||
|
description: str
|
||||||
|
icon: str
|
||||||
|
|
||||||
|
|
||||||
|
class Clouds(Model):
|
||||||
|
all: int
|
||||||
|
|
||||||
|
|
||||||
|
class Wind(Model):
|
||||||
|
speed: float
|
||||||
|
deg: int
|
||||||
|
gust: float
|
||||||
|
|
||||||
|
|
||||||
|
class Rain(Model):
|
||||||
|
interval_3h: float = Field(..., alias="3h")
|
||||||
|
|
||||||
|
|
||||||
|
class Sys(Model):
|
||||||
|
pod: str
|
||||||
|
|
||||||
|
|
||||||
|
class ForecastItem(Model):
|
||||||
|
dt: int
|
||||||
|
main: Main
|
||||||
|
weather: list[Weather]
|
||||||
|
clouds: Clouds
|
||||||
|
wind: Wind
|
||||||
|
visibility: int
|
||||||
|
pop: float
|
||||||
|
rain: Rain | None = None
|
||||||
|
sys: Sys
|
||||||
|
dt_txt: str
|
||||||
|
|
||||||
|
|
||||||
|
class Forecast(Model):
|
||||||
|
cod: str
|
||||||
|
message: int
|
||||||
|
cnt: int
|
||||||
|
list: list[ForecastItem]
|
||||||
|
|
||||||
|
|
||||||
|
class OpenWeather:
|
||||||
|
BASE_URL = "https://api.openweathermap.org"
|
||||||
|
|
||||||
|
def __init__(self, api_key: str):
|
||||||
|
self._api_key = api_key
|
||||||
|
self._source = ApiSource(self.BASE_URL)
|
||||||
|
|
||||||
|
async def get_forecast(self, lat: float, lon: float) -> Forecast:
|
||||||
|
endpoint = (
|
||||||
|
f"data/2.5/forecast?lat={lat}&lon={lon}&appid={self._api_key}&units=metric"
|
||||||
|
)
|
||||||
|
response = await self._source.request(endpoint)
|
||||||
|
response_data = json.loads(response)
|
||||||
|
return Forecast(**response_data)
|
||||||
52
gallery/painting/openweather/parser.py
Normal file
52
gallery/painting/openweather/parser.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from gallery.sketch.weather.model import Cloudness, Precipitation, WeatherValue
|
||||||
|
from gallery.sketch.weather.util import build_weather_value
|
||||||
|
|
||||||
|
from .openweather import ForecastItem
|
||||||
|
|
||||||
|
|
||||||
|
class ForecastItemParser:
|
||||||
|
CLOUDNESS_MAP: dict[str, Cloudness] = {
|
||||||
|
"clear sky": Cloudness.CLEAR,
|
||||||
|
"few clouds": Cloudness.PARTLY_CLOUDY,
|
||||||
|
"scattered clouds": Cloudness.PARTLY_CLOUDY,
|
||||||
|
"broken clouds": Cloudness.CLOUDY,
|
||||||
|
"overcast clouds": Cloudness.MAINLY_CLOUDY,
|
||||||
|
"light rain": Cloudness.CLOUDY,
|
||||||
|
}
|
||||||
|
|
||||||
|
PRECIPITATION_MAP: dict[str, Precipitation] = {
|
||||||
|
"light rain": Precipitation.SMALL_RAIN,
|
||||||
|
"rain": Precipitation.RAIN,
|
||||||
|
"heavy rain": Precipitation.SHOWER,
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse(self, item: ForecastItem) -> WeatherValue:
|
||||||
|
item_date = datetime.datetime.fromtimestamp(item.dt, datetime.UTC)
|
||||||
|
item_date = (
|
||||||
|
item_date.replace(tzinfo=datetime.timezone.utc)
|
||||||
|
.astimezone(tz=None)
|
||||||
|
.replace(tzinfo=None)
|
||||||
|
)
|
||||||
|
value = build_weather_value(item_date)
|
||||||
|
# TODO parse temperature interval flag
|
||||||
|
value.temperature = [round(item.main.temp)]
|
||||||
|
# value.temperature = [round(item.main.temp_max), round(item.main.temp_min)]
|
||||||
|
value.pressure = [round(item.main.pressure / 133.3 * 100)]
|
||||||
|
value.humidity = item.main.humidity
|
||||||
|
value.wind_speed = round(item.wind.speed)
|
||||||
|
value.wind_gust = round(item.wind.gust)
|
||||||
|
value.wind_direction = item.wind.deg
|
||||||
|
value.sky.cloudness = self.CLOUDNESS_MAP.get(
|
||||||
|
item.weather[0].description, Cloudness.CLEAR
|
||||||
|
)
|
||||||
|
value.sky.precipitation = self.PRECIPITATION_MAP.get(
|
||||||
|
item.weather[0].description, Precipitation.NO
|
||||||
|
)
|
||||||
|
if item.rain:
|
||||||
|
value.precipitation = round(item.rain.interval_3h, 1)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
FORECAST_ITEM_PARSER = ForecastItemParser()
|
||||||
0
gallery/painting/yandextv/__init__.py
Normal file
0
gallery/painting/yandextv/__init__.py
Normal file
94
gallery/painting/yandextv/api.py
Normal file
94
gallery/painting/yandextv/api.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from gallery.sketch.schedule.api import ScheduleApi
|
||||||
|
from gallery.sketch.schedule.model import Channel, ChannelId, Schedule, ScheduleValue
|
||||||
|
from gallery.sketch.source import ApiSource
|
||||||
|
|
||||||
|
logger = logging.getLogger("matchtv")
|
||||||
|
|
||||||
|
CHANNELS_MAP: dict[ChannelId, str] = {
|
||||||
|
ChannelId.MATCH_TV: "match-tv-49",
|
||||||
|
ChannelId.MATCH_IGRA: "match-igra-1174",
|
||||||
|
ChannelId.MATCH_ARENA: "match-arena-1173",
|
||||||
|
ChannelId.MATCH_FUTBOL_1: "match-futbol-1-646",
|
||||||
|
ChannelId.MATCH_FUTBOL_2: "match-futbol-2-593",
|
||||||
|
ChannelId.MATCH_FUTBOL_3: "match-futbol-3-797",
|
||||||
|
ChannelId.MATCH_STRANA: "match-strana-1356",
|
||||||
|
ChannelId.MATCH_PLANETA: "match-planeta-1177",
|
||||||
|
# ChannelId.EUROSPORT: "eurosport-677",
|
||||||
|
# ChannelId.EUROSPORT_2: "eurosport-2-720",
|
||||||
|
ChannelId.START: "start-103",
|
||||||
|
}
|
||||||
|
|
||||||
|
HEADERS: dict[str, str] = {
|
||||||
|
"Accept": (
|
||||||
|
"text/html,"
|
||||||
|
"application/xhtml+xml,"
|
||||||
|
"application/xml;q=0.9,"
|
||||||
|
"image/avif,image/webp,"
|
||||||
|
"image/apng,*/*;q=0.8,"
|
||||||
|
"application/signed-exchange;v=b3;q=0.9"
|
||||||
|
),
|
||||||
|
"Accept-Encoding": "gzip, deflate, br",
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Host": "tv.yandex.ru",
|
||||||
|
"sec-ch-ua": '"Chromium";v="100", " Not A;Brand";v="99"',
|
||||||
|
"sec-ch-ua-mobile": "?0",
|
||||||
|
"sec-ch-ua-platform": '"Linux"',
|
||||||
|
"Sec-Fetch-Dest": "document",
|
||||||
|
"Sec-Fetch-Mode": "navigate",
|
||||||
|
"Sec-Fetch-Site": "none",
|
||||||
|
"Sec-Fetch-User": "?1",
|
||||||
|
"Upgrade-Insecure-Requests": "1",
|
||||||
|
"User-Agent": (
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/100.0.4896.133 "
|
||||||
|
"Safari/537.36"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class YandexTvApi(ScheduleApi):
|
||||||
|
PROVIDER = "yandextv"
|
||||||
|
SOURCE = ApiSource("https://tv.yandex.ru", headers=HEADERS)
|
||||||
|
|
||||||
|
async def get_channels(self) -> list[ChannelId]:
|
||||||
|
return list(CHANNELS_MAP.keys())
|
||||||
|
|
||||||
|
async def get_channel_schedule(
|
||||||
|
self, channel_id: ChannelId, date: datetime.date
|
||||||
|
) -> Schedule:
|
||||||
|
endpoint = f"channel/{CHANNELS_MAP[channel_id]}?date={date:%Y-%m-%d}"
|
||||||
|
data = await self.SOURCE.request(endpoint)
|
||||||
|
soup = BeautifulSoup(data, features="html.parser")
|
||||||
|
if soup.select_one(".CheckboxCaptcha") is not None:
|
||||||
|
raise RuntimeError("Captcha")
|
||||||
|
values = []
|
||||||
|
channel_name = soup.select_one(".channel-header__text").text.strip()
|
||||||
|
current_day = datetime.datetime.combine(
|
||||||
|
date.today(), datetime.datetime.min.time()
|
||||||
|
)
|
||||||
|
end = current_day + datetime.timedelta(days=1, hours=6)
|
||||||
|
prev_value: ScheduleValue | None = None
|
||||||
|
for item in soup.select(".channel-schedule .channel-schedule__event"):
|
||||||
|
title = item.select_one(".channel-schedule__title").text.strip()
|
||||||
|
time_str = item.select_one(".channel-schedule__time").text.strip()
|
||||||
|
hours, minutes = map(int, time_str.split(":"))
|
||||||
|
item_date = current_day.replace(hour=hours, minute=minutes)
|
||||||
|
if prev_value is not None and item_date.hour < prev_value.start.hour:
|
||||||
|
current_day += datetime.timedelta(days=1)
|
||||||
|
item_date += datetime.timedelta(days=1)
|
||||||
|
live = item.select_one(".channel-schedule__info .icon_live") is not None
|
||||||
|
value = ScheduleValue(start=item_date, end=end, label=title, live=live)
|
||||||
|
values.append(value)
|
||||||
|
if prev_value is not None:
|
||||||
|
prev_value.end = item_date
|
||||||
|
prev_value = value
|
||||||
|
return Schedule(
|
||||||
|
channel=Channel(id=channel_id, name=channel_name), date=date, values=values
|
||||||
|
)
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
|
||||||
class Api:
|
class Api:
|
||||||
PROVIDER: str
|
PROVIDER: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def provider(self) -> str:
|
def provider(self) -> str:
|
||||||
return self.PROVIDER
|
return self.PROVIDER
|
||||||
|
|
||||||
|
|
||||||
|
API = TypeVar("API", bound=Api)
|
||||||
|
|||||||
28
gallery/sketch/bundle.py
Normal file
28
gallery/sketch/bundle.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from .api import API, Api
|
||||||
|
from .schedule.api import ScheduleApi
|
||||||
|
from .weather.api import WeatherApi
|
||||||
|
|
||||||
|
|
||||||
|
class ApiBundle(list[Api]):
|
||||||
|
def __init__(self, values: list[Api]) -> None:
|
||||||
|
super().__init__(values)
|
||||||
|
|
||||||
|
def get_api_by_provider(self, provider: str) -> Api:
|
||||||
|
for value in self:
|
||||||
|
if value.PROVIDER == provider:
|
||||||
|
return value
|
||||||
|
raise ValueError(provider)
|
||||||
|
|
||||||
|
def get_api_by_type(self, api_type: type[API]) -> API:
|
||||||
|
for value in self:
|
||||||
|
if isinstance(value, api_type):
|
||||||
|
return value
|
||||||
|
raise ValueError(api_type)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weather(self) -> WeatherApi:
|
||||||
|
return self.get_api_by_type(WeatherApi)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schedule(self) -> ScheduleApi:
|
||||||
|
return self.get_api_by_type(ScheduleApi)
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
from typing import Generic, TypeVar
|
from typing import Generic, NamedTuple
|
||||||
|
|
||||||
from gallery.util import TimeUnit
|
from gallery.util import TimeUnit
|
||||||
|
|
||||||
from .api import Api
|
from .api import API, Api
|
||||||
|
|
||||||
API = TypeVar("API", bound=Api)
|
|
||||||
|
class CachePreset(NamedTuple):
|
||||||
|
ttl: int = TimeUnit.HOUR
|
||||||
|
alias: str = "redis"
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CACHE_PRESET = CachePreset()
|
||||||
|
|
||||||
|
|
||||||
class CachedApi(Api, Generic[API]):
|
class CachedApi(Api, Generic[API]):
|
||||||
CACHE_TTL: int = TimeUnit.HOUR
|
|
||||||
CACHE_ALIAS: str = "redis"
|
|
||||||
CACHE_KEY: str
|
CACHE_KEY: str
|
||||||
|
|
||||||
def __init__(self, api: API):
|
def __init__(self, api: API):
|
||||||
|
|||||||
@@ -7,5 +7,8 @@ class CatalogBundle(Generic[T]):
|
|||||||
def __init__(self, items: list[T]) -> None:
|
def __init__(self, items: list[T]) -> None:
|
||||||
self._items_by_id = {item.id: item for item in items}
|
self._items_by_id = {item.id: item for item in items}
|
||||||
|
|
||||||
|
def get_item(self, item_id: str) -> T:
|
||||||
|
return self._items_by_id[item_id]
|
||||||
|
|
||||||
def select_items(self, ids: list[str]) -> list[T]:
|
def select_items(self, ids: list[str]) -> list[T]:
|
||||||
return [self._items_by_id[id_] for id_ in ids]
|
return [self._items_by_id[id_] for id_ in ids]
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class MockData:
|
|
||||||
|
|
||||||
def __init__(self, data_dir) -> None:
|
|
||||||
self._data_dir = data_dir
|
|
||||||
|
|
||||||
def get_html(self, key: str) -> str:
|
|
||||||
return (self._data_dir / f"{key}.html").read_text()
|
|
||||||
|
|
||||||
def get_json(self, key: str) -> dict:
|
|
||||||
data = json.loads((self._data_dir / f"{key}.json").read_text())
|
|
||||||
return data
|
|
||||||
@@ -1,14 +1,28 @@
|
|||||||
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from ..api import Api
|
from ..api import Api
|
||||||
from .model import Schedule
|
from .model import ChannelId, Schedule
|
||||||
|
|
||||||
|
|
||||||
class ScheduleApi(Api):
|
class ScheduleApi(Api):
|
||||||
async def get_channels(self) -> list[str]:
|
INTERVAL: float = 0.5
|
||||||
|
|
||||||
|
async def get_channels(self) -> list[ChannelId]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def get_channel_schedule(
|
async def get_channel_schedule(
|
||||||
self, channel_id: str, date: datetime.date
|
self, channel_id: ChannelId, date: datetime.date
|
||||||
) -> Schedule:
|
) -> Schedule:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
|
||||||
|
channels = await self.get_channels()
|
||||||
|
results = []
|
||||||
|
for channel in channels:
|
||||||
|
results.append(
|
||||||
|
await self.get_channel_schedule(channel_id=channel, date=date)
|
||||||
|
)
|
||||||
|
if self.INTERVAL > 0:
|
||||||
|
await asyncio.sleep(self.INTERVAL)
|
||||||
|
return results
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import datetime
|
|||||||
|
|
||||||
from aiocache import cached
|
from aiocache import cached
|
||||||
|
|
||||||
from gallery.sketch.cached import CachedApi
|
from gallery.sketch.cached import CachedApi, CachePreset
|
||||||
|
from gallery.util import TimeUnit
|
||||||
|
|
||||||
from .api import ScheduleApi
|
from .api import ScheduleApi
|
||||||
from .model import Schedule
|
from .model import ChannelId, Schedule
|
||||||
|
|
||||||
|
CACHE_PRESET = CachePreset(ttl=TimeUnit.HOUR * 6)
|
||||||
|
|
||||||
|
|
||||||
class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
|
class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
|
||||||
@@ -13,20 +16,27 @@ class CachedScheduleApi(ScheduleApi, CachedApi[ScheduleApi]):
|
|||||||
|
|
||||||
@cached(
|
@cached(
|
||||||
key_builder=lambda fun, self: f"api.{self.CACHE_KEY}.{self.provider}.channels",
|
key_builder=lambda fun, self: f"api.{self.CACHE_KEY}.{self.provider}.channels",
|
||||||
alias=CachedApi.CACHE_ALIAS,
|
**CACHE_PRESET._asdict(),
|
||||||
ttl=CachedApi.CACHE_TTL,
|
|
||||||
)
|
)
|
||||||
async def get_channels(self) -> list[str]:
|
async def get_channels(self) -> list[ChannelId]:
|
||||||
return await self._api.get_channels()
|
return await self._api.get_channels()
|
||||||
|
|
||||||
@cached(
|
@cached(
|
||||||
key_builder=lambda fun, self, channel_id, date: (
|
key_builder=lambda fun, self, channel_id, date: (
|
||||||
f"api.{self.CACHE_KEY}.{self.provider}.channel.{channel_id}.{date}"
|
f"api.{self.CACHE_KEY}.{self.provider}.channel.{channel_id}.{date}"
|
||||||
),
|
),
|
||||||
alias=CachedApi.CACHE_ALIAS,
|
**CACHE_PRESET._asdict(),
|
||||||
ttl=CachedApi.CACHE_TTL,
|
|
||||||
)
|
)
|
||||||
async def get_channel_schedule(
|
async def get_channel_schedule(
|
||||||
self, channel_id: str, date: datetime.date
|
self, channel_id: ChannelId, date: datetime.date
|
||||||
) -> Schedule:
|
) -> Schedule:
|
||||||
return await self._api.get_channel_schedule(channel_id, date)
|
return await self._api.get_channel_schedule(channel_id, date)
|
||||||
|
|
||||||
|
@cached(
|
||||||
|
key_builder=lambda fun, self, date: (
|
||||||
|
f"api.{self.CACHE_KEY}.{self.provider}.all.{date}"
|
||||||
|
),
|
||||||
|
**CACHE_PRESET._asdict(),
|
||||||
|
)
|
||||||
|
async def get_all_schedules(self, date: datetime.date) -> list[Schedule]:
|
||||||
|
return await self._api.get_all_schedules(date)
|
||||||
|
|||||||
@@ -1,22 +1,6 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
from gallery.sketch.catalog import CatalogBundle
|
from gallery.sketch.catalog import CatalogBundle
|
||||||
|
|
||||||
from .model import Channel
|
from .model import Channel, ChannelId
|
||||||
|
|
||||||
|
|
||||||
class ChannelId(str, Enum):
|
|
||||||
MATCH_TV = "matchtv"
|
|
||||||
MATCH_IGRA = "igra"
|
|
||||||
MATCH_ARENA = "arena"
|
|
||||||
MATCH_FUTBOL_1 = "futbol-1"
|
|
||||||
MATCH_FUTBOL_2 = "futbol-2"
|
|
||||||
MATCH_FUTBOL_3 = "futbol-3"
|
|
||||||
MATCH_STRANA = "strana"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
BUNDLE = CatalogBundle(
|
BUNDLE = CatalogBundle(
|
||||||
[
|
[
|
||||||
@@ -27,5 +11,11 @@ BUNDLE = CatalogBundle(
|
|||||||
Channel(id=ChannelId.MATCH_FUTBOL_2, name="Футбол 2"),
|
Channel(id=ChannelId.MATCH_FUTBOL_2, name="Футбол 2"),
|
||||||
Channel(id=ChannelId.MATCH_FUTBOL_3, name="Футбол 3"),
|
Channel(id=ChannelId.MATCH_FUTBOL_3, name="Футбол 3"),
|
||||||
Channel(id=ChannelId.MATCH_STRANA, name="Матч! Страна"),
|
Channel(id=ChannelId.MATCH_STRANA, name="Матч! Страна"),
|
||||||
|
Channel(id=ChannelId.MATCH_PLANETA, name="Матч! Планета"),
|
||||||
|
Channel(id=ChannelId.MATCH_PLANETA, name="Матч! Планета"),
|
||||||
|
Channel(id=ChannelId.EUROSPORT, name="Europsort"),
|
||||||
|
Channel(id=ChannelId.EUROSPORT_2, name="Europsort 2"),
|
||||||
|
Channel(id=ChannelId.START, name="Старт!"),
|
||||||
|
Channel(id=ChannelId.TEST, name="Тест"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -8,8 +9,26 @@ class Model(BaseModel):
|
|||||||
use_enum_values = True
|
use_enum_values = True
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelId(StrEnum):
|
||||||
|
MATCH_TV = "matchtv"
|
||||||
|
MATCH_IGRA = "igra"
|
||||||
|
MATCH_ARENA = "arena"
|
||||||
|
MATCH_FUTBOL_1 = "futbol-1"
|
||||||
|
MATCH_FUTBOL_2 = "futbol-2"
|
||||||
|
MATCH_FUTBOL_3 = "futbol-3"
|
||||||
|
MATCH_STRANA = "strana"
|
||||||
|
MATCH_PLANETA = "planeta"
|
||||||
|
EUROSPORT = "eurosport"
|
||||||
|
EUROSPORT_2 = "eurosport-2"
|
||||||
|
START = "start"
|
||||||
|
TEST = "test"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
class Channel(Model):
|
class Channel(Model):
|
||||||
id: str
|
id: ChannelId
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,18 +19,18 @@ class ApiSource:
|
|||||||
user_agent: str = DEFAULT_USER_AGENT,
|
user_agent: str = DEFAULT_USER_AGENT,
|
||||||
timeout: float = DEFAULT_TIMEOUT,
|
timeout: float = DEFAULT_TIMEOUT,
|
||||||
cookies: dict[str, str] | None = None,
|
cookies: dict[str, str] | None = None,
|
||||||
|
headers: dict[str, str] | None = None,
|
||||||
):
|
):
|
||||||
self._base_url = base_url
|
self._base_url = base_url
|
||||||
self._user_agent = user_agent
|
self._user_agent = user_agent
|
||||||
self._timeout = timeout
|
self._timeout = timeout
|
||||||
self._cookies = cookies
|
self._cookies = cookies
|
||||||
|
self._headers = headers
|
||||||
|
|
||||||
async def request(self, endpoint: str) -> str:
|
async def request(self, endpoint: str) -> str:
|
||||||
url = f"{self._base_url}/{endpoint}"
|
url = f"{self._base_url}/{endpoint}"
|
||||||
logger.info(url)
|
logger.info(url)
|
||||||
headers = {
|
headers = {"User-Agent": self._user_agent, **(self._headers or {})}
|
||||||
"User-Agent": self._user_agent,
|
|
||||||
}
|
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(
|
||||||
headers=headers,
|
headers=headers,
|
||||||
cookies=self._cookies,
|
cookies=self._cookies,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from ..api import Api
|
from ..api import Api
|
||||||
from .model import WeatherResponse
|
from .model import Location, WeatherResponse
|
||||||
|
|
||||||
|
|
||||||
class WeatherApi(Api):
|
class WeatherApi(Api):
|
||||||
|
|
||||||
async def get_locations(self) -> list[str]:
|
async def find_locations(self, query: str) -> list[Location]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
||||||
|
|||||||
@@ -2,29 +2,29 @@ import datetime
|
|||||||
|
|
||||||
from aiocache import cached
|
from aiocache import cached
|
||||||
|
|
||||||
from gallery.sketch.cached import CachedApi
|
from gallery.sketch.cached import DEFAULT_CACHE_PRESET, CachedApi
|
||||||
|
|
||||||
from .api import WeatherApi
|
from .api import WeatherApi
|
||||||
from .model import WeatherResponse
|
from .model import Location, WeatherResponse
|
||||||
|
|
||||||
|
CACHE_PRESET = DEFAULT_CACHE_PRESET
|
||||||
|
|
||||||
|
|
||||||
class CachedWeatherApi(WeatherApi, CachedApi[WeatherApi]):
|
class CachedWeatherApi(WeatherApi, CachedApi[WeatherApi]):
|
||||||
CACHE_KEY = "weather"
|
CACHE_KEY = "weather"
|
||||||
|
|
||||||
@cached(
|
@cached(
|
||||||
key_builder=lambda fun, self: f"api.{self.CACHE_KEY}.{self.provider}.locations",
|
key_builder=lambda fun, self, query: f"api.{self.CACHE_KEY}.{self.provider}.locations.{query}",
|
||||||
alias=CachedApi.CACHE_ALIAS,
|
**CACHE_PRESET._asdict(),
|
||||||
ttl=CachedApi.CACHE_TTL,
|
|
||||||
)
|
)
|
||||||
async def get_locations(self) -> list[str]:
|
async def find_locations(self, query: str) -> list[Location]:
|
||||||
return await self._api.get_locations()
|
return await self._api.find_locations(query)
|
||||||
|
|
||||||
@cached(
|
@cached(
|
||||||
key_builder=lambda fun, self, location_id, date: (
|
key_builder=lambda fun, self, location_id, date: (
|
||||||
f"api.{self.CACHE_KEY}.{self.provider}.day.{location_id}.{date}"
|
f"api.{self.CACHE_KEY}.{self.provider}.day.{location_id}.{date}"
|
||||||
),
|
),
|
||||||
alias=CachedApi.CACHE_ALIAS,
|
**CACHE_PRESET._asdict(),
|
||||||
ttl=CachedApi.CACHE_TTL,
|
|
||||||
)
|
)
|
||||||
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
||||||
return await self._api.get_day(location_id, date)
|
return await self._api.get_day(location_id, date)
|
||||||
@@ -33,8 +33,7 @@ class CachedWeatherApi(WeatherApi, CachedApi[WeatherApi]):
|
|||||||
key_builder=lambda fun, self, location_id, date: (
|
key_builder=lambda fun, self, location_id, date: (
|
||||||
f"api.{self.CACHE_KEY}.{self.provider}.day.{location_id}.{date}"
|
f"api.{self.CACHE_KEY}.{self.provider}.day.{location_id}.{date}"
|
||||||
),
|
),
|
||||||
alias=CachedApi.CACHE_ALIAS,
|
**CACHE_PRESET._asdict(),
|
||||||
ttl=CachedApi.CACHE_TTL,
|
|
||||||
)
|
)
|
||||||
async def get_days(self, location_id: str, days: int) -> WeatherResponse:
|
async def get_days(self, location_id: str, days: int) -> WeatherResponse:
|
||||||
return await self._api.get_days(location_id, days)
|
return await self._api.get_days(location_id, days)
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
from enum import Enum
|
|
||||||
|
|
||||||
from gallery.sketch.catalog import CatalogBundle
|
|
||||||
|
|
||||||
from .model import Location
|
|
||||||
|
|
||||||
|
|
||||||
class LocationId(str, Enum):
|
|
||||||
OREL = "orel-4432"
|
|
||||||
ZMIYEVKA = "zmiyevka-184640"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
BUNDLE = CatalogBundle(
|
|
||||||
[
|
|
||||||
Location(id=LocationId.OREL, name="Орёл"),
|
|
||||||
Location(id=LocationId.ZMIYEVKA, name="Змиёвка"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from gallery.sketch.mock import MockData
|
|
||||||
from gallery.sketch.weather.model import WeatherResponse
|
|
||||||
|
|
||||||
|
|
||||||
class WeatherMockData(MockData):
|
|
||||||
def get_response(self, key: str) -> WeatherResponse:
|
|
||||||
return WeatherResponse(**self.get_json(key))
|
|
||||||
|
|
||||||
|
|
||||||
WEATHER_MOCK_DATA = WeatherMockData(Path(__file__).parent / "data")
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"location":"Орел","date":"2024-07-29","period":"day","values":[{"date":"2024-07-29T00:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[20],"wind_speed":1,"wind_gust":1,"wind_direction":"SW","precipitation":0.0,"pressure":[744],"humidity":85},{"date":"2024-07-29T03:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[18],"wind_speed":1,"wind_gust":1,"wind_direction":"W","precipitation":0.6,"pressure":[742],"humidity":96},{"date":"2024-07-29T06:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"rain","thunder":false,"fog":false},"temperature":[19],"wind_speed":1,"wind_gust":2,"wind_direction":"S","precipitation":4.9,"pressure":[741],"humidity":95},{"date":"2024-07-29T09:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"rain","thunder":false,"fog":false},"temperature":[19],"wind_speed":3,"wind_gust":7,"wind_direction":"S","precipitation":3.8,"pressure":[740],"humidity":83},{"date":"2024-07-29T12:00:00","sky":{"cloudness":"clear","precipitation":"no","thunder":false,"fog":false},"temperature":[21],"wind_speed":4,"wind_gust":11,"wind_direction":"W","precipitation":0.0,"pressure":[740],"humidity":54},{"date":"2024-07-29T15:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[21],"wind_speed":4,"wind_gust":10,"wind_direction":"SW","precipitation":0.0,"pressure":[738],"humidity":48},{"date":"2024-07-29T18:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[19],"wind_speed":3,"wind_gust":10,"wind_direction":"SW","precipitation":0.0,"pressure":[737],"humidity":63},{"date":"2024-07-29T21:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[17],"wind_speed":3,"wind_gust":7,"wind_direction":"SW","precipitation":0.0,"pressure":[737],"humidity":77}]}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"location":"Орел","date":"2024-07-29","period":"days","values":[{"date":"2024-07-29T00:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"rain","thunder":false,"fog":false},"temperature":[21,17],"wind_speed":4,"wind_gust":11,"wind_direction":"W","precipitation":9.3,"pressure":[744,737],"humidity":96},{"date":"2024-07-30T00:00:00","sky":{"cloudness":"mainly_cloudy","precipitation":"rain","thunder":true,"fog":false},"temperature":[19,14],"wind_speed":2,"wind_gust":7,"wind_direction":"N","precipitation":11.0,"pressure":[737,733],"humidity":100},{"date":"2024-07-31T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[22,14],"wind_speed":3,"wind_gust":10,"wind_direction":"NW","precipitation":1.8,"pressure":[741,738],"humidity":99},{"date":"2024-07-01T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[24,14],"wind_speed":3,"wind_gust":10,"wind_direction":"W","precipitation":0.1,"pressure":[741,740],"humidity":97},{"date":"2024-07-02T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[24,17],"wind_speed":2,"wind_gust":8,"wind_direction":"W","precipitation":0.2,"pressure":[740],"humidity":84},{"date":"2024-07-03T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[25,14],"wind_speed":1,"wind_gust":4,"wind_direction":"N","precipitation":0.0,"pressure":[740,739],"humidity":99},{"date":"2024-07-04T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"no","thunder":false,"fog":false},"temperature":[25,14],"wind_speed":3,"wind_gust":6,"wind_direction":"N","precipitation":0.0,"pressure":[743,740],"humidity":92},{"date":"2024-07-05T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":true,"fog":false},"temperature":[25,15],"wind_speed":3,"wind_gust":7,"wind_direction":"NW","precipitation":2.1,"pressure":[744,743],"humidity":98},{"date":"2024-07-06T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[24,14],"wind_speed":3,"wind_gust":5,"wind_direction":"NW","precipitation":0.3,"pressure":[745,744],"humidity":98},{"date":"2024-07-07T00:00:00","sky":{"cloudness":"party_cloudy","precipitation":"small_rain","thunder":false,"fog":false},"temperature":[26,14],"wind_speed":2,"wind_gust":5,"wind_direction":"NW","precipitation":0.2,"pressure":[747,745],"humidity":95}]}
|
|
||||||
@@ -12,6 +12,11 @@ class Model(BaseModel):
|
|||||||
class Location(Model):
|
class Location(Model):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
country: str
|
||||||
|
district: str
|
||||||
|
subdistrict: str
|
||||||
|
|
||||||
|
|
||||||
class Cloudness(str, Enum):
|
class Cloudness(str, Enum):
|
||||||
@@ -25,7 +30,10 @@ class Precipitation(str, Enum):
|
|||||||
NO = "no"
|
NO = "no"
|
||||||
SMALL_RAIN = "small_rain"
|
SMALL_RAIN = "small_rain"
|
||||||
RAIN = "rain"
|
RAIN = "rain"
|
||||||
|
HEAVY_RAIN = "heavy_rain"
|
||||||
SHOWER = "shower"
|
SHOWER = "shower"
|
||||||
|
SNOW = "snow"
|
||||||
|
HEAVY_SNOW = "heavy_snow"
|
||||||
|
|
||||||
|
|
||||||
class Sky(Model):
|
class Sky(Model):
|
||||||
@@ -38,22 +46,69 @@ class Sky(Model):
|
|||||||
class WindDirection(str, Enum):
|
class WindDirection(str, Enum):
|
||||||
CALM = "calm"
|
CALM = "calm"
|
||||||
N = "N"
|
N = "N"
|
||||||
NO = "NO"
|
NE = "NE"
|
||||||
O = "O"
|
E = "E"
|
||||||
SO = "SO"
|
SE = "SE"
|
||||||
S = "S"
|
S = "S"
|
||||||
SW = "SW"
|
SW = "SW"
|
||||||
W = "W"
|
W = "W"
|
||||||
NW = "NW"
|
NW = "NW"
|
||||||
|
|
||||||
|
|
||||||
|
class WindDirectionDeg(float):
|
||||||
|
@property
|
||||||
|
def direction(self) -> WindDirection:
|
||||||
|
return self.to_direction()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float:
|
||||||
|
return self
|
||||||
|
|
||||||
|
# pylint:disable=too-many-return-statements
|
||||||
|
def to_direction(self) -> WindDirection:
|
||||||
|
if self > 337.5 or self <= 22.25:
|
||||||
|
return WindDirection.N
|
||||||
|
elif self <= 67.5:
|
||||||
|
return WindDirection.NE
|
||||||
|
elif self <= 112.5:
|
||||||
|
return WindDirection.E
|
||||||
|
elif self <= 157.5:
|
||||||
|
return WindDirection.SE
|
||||||
|
elif self <= 202.5:
|
||||||
|
return WindDirection.S
|
||||||
|
elif self <= 247.5:
|
||||||
|
return WindDirection.SW
|
||||||
|
elif self <= 292.5:
|
||||||
|
return WindDirection.W
|
||||||
|
elif self <= 337.5:
|
||||||
|
return WindDirection.NW
|
||||||
|
else:
|
||||||
|
return WindDirection.CALM
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_direction(cls, direction: WindDirection) -> "WindDirectionDeg":
|
||||||
|
return cls(
|
||||||
|
{
|
||||||
|
WindDirection.CALM: -1,
|
||||||
|
WindDirection.N: 0,
|
||||||
|
WindDirection.NE: 45,
|
||||||
|
WindDirection.E: 90,
|
||||||
|
WindDirection.SE: 135,
|
||||||
|
WindDirection.S: 180,
|
||||||
|
WindDirection.SW: 225,
|
||||||
|
WindDirection.W: 270,
|
||||||
|
WindDirection.NW: 315,
|
||||||
|
}[direction]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WeatherValue(Model):
|
class WeatherValue(Model):
|
||||||
date: datetime.datetime
|
date: datetime.datetime
|
||||||
sky: Sky
|
sky: Sky
|
||||||
temperature: list[int]
|
temperature: list[int]
|
||||||
wind_speed: int
|
wind_speed: int
|
||||||
wind_gust: int
|
wind_gust: int
|
||||||
wind_direction: WindDirection
|
wind_direction: float
|
||||||
precipitation: float
|
precipitation: float
|
||||||
pressure: list[int]
|
pressure: list[int]
|
||||||
humidity: int
|
humidity: int
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import statistics
|
||||||
|
|
||||||
from .model import Cloudness, Precipitation, Sky, WeatherValue, WindDirection
|
from .model import Cloudness, Precipitation, Sky, WeatherValue, WindDirectionDeg
|
||||||
|
|
||||||
|
|
||||||
def build_weather_value(date: datetime.datetime) -> WeatherValue:
|
def build_weather_value(date: datetime.datetime) -> WeatherValue:
|
||||||
@@ -15,8 +16,49 @@ def build_weather_value(date: datetime.datetime) -> WeatherValue:
|
|||||||
temperature=[],
|
temperature=[],
|
||||||
wind_speed=0,
|
wind_speed=0,
|
||||||
wind_gust=0,
|
wind_gust=0,
|
||||||
wind_direction=WindDirection.CALM,
|
wind_direction=WindDirectionDeg(-1),
|
||||||
precipitation=0,
|
precipitation=0,
|
||||||
pressure=[],
|
pressure=[],
|
||||||
humidity=0,
|
humidity=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def merge_weather_values(
|
||||||
|
date: datetime.datetime, values: list[WeatherValue]
|
||||||
|
) -> WeatherValue:
|
||||||
|
result = build_weather_value(date)
|
||||||
|
temperatures = []
|
||||||
|
pressures = []
|
||||||
|
humidities = []
|
||||||
|
wind_speeds = []
|
||||||
|
wind_gusts = []
|
||||||
|
wind_directions = []
|
||||||
|
cloudnesses = []
|
||||||
|
precipitations = []
|
||||||
|
precipitation = 0
|
||||||
|
for value in values:
|
||||||
|
temperatures += value.temperature
|
||||||
|
pressures += value.pressure
|
||||||
|
humidities.append(value.humidity)
|
||||||
|
wind_speeds.append(value.wind_speed)
|
||||||
|
wind_gusts.append(value.wind_gust)
|
||||||
|
wind_directions.append(value.wind_direction)
|
||||||
|
cloudnesses.append(value.sky.cloudness)
|
||||||
|
precipitations.append(value.sky.precipitation)
|
||||||
|
precipitation += value.precipitation
|
||||||
|
result.temperature = [max(temperatures), min(temperatures)]
|
||||||
|
result.pressure = [max(pressures), min(pressures)]
|
||||||
|
result.humidity = round(statistics.mean(humidities))
|
||||||
|
result.wind_speed = round(statistics.mean(wind_speeds))
|
||||||
|
result.wind_gust = round(statistics.mean(wind_gusts))
|
||||||
|
result.wind_direction = statistics.mean(wind_directions)
|
||||||
|
# TODO: merge cloudnesses
|
||||||
|
for item in cloudnesses:
|
||||||
|
if item != Cloudness.CLEAR:
|
||||||
|
result.sky.cloudness = item
|
||||||
|
# TODO: merge precipitations
|
||||||
|
for item in precipitations:
|
||||||
|
if item != Precipitation.NO:
|
||||||
|
result.sky.precipitation = item
|
||||||
|
result.precipitation = precipitation
|
||||||
|
return result
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
class TimeUnit:
|
class TimeUnit:
|
||||||
SECOND = 1
|
SECOND = 1
|
||||||
MINUTE = 60 * SECOND
|
MINUTE = 60 * SECOND
|
||||||
HOUR = 60 * MINUTE
|
HOUR = 60 * MINUTE
|
||||||
DAY = 24 * HOUR
|
DAY = 24 * HOUR
|
||||||
|
|
||||||
|
|
||||||
|
root_path = Path(__file__).parent.parent
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.1.0"
|
__version__ = "0.2.1"
|
||||||
|
|||||||
91
poetry.lock
generated
91
poetry.lock
generated
@@ -1,4 +1,4 @@
|
|||||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiocache"
|
name = "aiocache"
|
||||||
@@ -6,6 +6,7 @@ version = "0.12.2"
|
|||||||
description = "multi backend asyncio cache"
|
description = "multi backend asyncio cache"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"},
|
{file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"},
|
||||||
{file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"},
|
{file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"},
|
||||||
@@ -25,6 +26,7 @@ version = "3.9.5"
|
|||||||
description = "Async http client/server framework (asyncio)"
|
description = "Async http client/server framework (asyncio)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
|
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
|
||||||
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
|
{file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
|
||||||
@@ -112,7 +114,7 @@ multidict = ">=4.5,<7.0"
|
|||||||
yarl = ">=1.0,<2.0"
|
yarl = ">=1.0,<2.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
speedups = ["Brotli", "aiodns", "brotlicffi"]
|
speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiosignal"
|
name = "aiosignal"
|
||||||
@@ -120,6 +122,7 @@ version = "1.3.1"
|
|||||||
description = "aiosignal: a list of registered asynchronous callbacks"
|
description = "aiosignal: a list of registered asynchronous callbacks"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
|
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
|
||||||
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
|
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
|
||||||
@@ -134,6 +137,7 @@ version = "0.7.0"
|
|||||||
description = "Reusable constraint types to use with typing.Annotated"
|
description = "Reusable constraint types to use with typing.Annotated"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main", "app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||||
@@ -145,6 +149,7 @@ version = "4.4.0"
|
|||||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
|
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
|
||||||
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
|
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
|
||||||
@@ -156,7 +161,7 @@ sniffio = ">=1.1"
|
|||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||||
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""]
|
||||||
trio = ["trio (>=0.23)"]
|
trio = ["trio (>=0.23)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -165,6 +170,7 @@ version = "3.2.4"
|
|||||||
description = "An abstract syntax tree for Python with inference support."
|
description = "An abstract syntax tree for Python with inference support."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.8.0"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"},
|
{file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"},
|
||||||
{file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"},
|
{file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"},
|
||||||
@@ -176,6 +182,7 @@ version = "23.2.0"
|
|||||||
description = "Classes Without Boilerplate"
|
description = "Classes Without Boilerplate"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
|
{file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
|
||||||
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
|
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
|
||||||
@@ -186,8 +193,8 @@ cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
|
|||||||
dev = ["attrs[tests]", "pre-commit"]
|
dev = ["attrs[tests]", "pre-commit"]
|
||||||
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
|
docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
|
||||||
tests = ["attrs[tests-no-zope]", "zope-interface"]
|
tests = ["attrs[tests-no-zope]", "zope-interface"]
|
||||||
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
|
tests-mypy = ["mypy (>=1.6) ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\""]
|
||||||
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
|
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "beautifulsoup4"
|
name = "beautifulsoup4"
|
||||||
@@ -195,6 +202,7 @@ version = "4.12.3"
|
|||||||
description = "Screen-scraping library"
|
description = "Screen-scraping library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6.0"
|
python-versions = ">=3.6.0"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
|
{file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
|
||||||
{file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
|
{file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
|
||||||
@@ -216,6 +224,7 @@ version = "24.4.2"
|
|||||||
description = "The uncompromising code formatter."
|
description = "The uncompromising code formatter."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
|
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
|
||||||
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
|
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
|
||||||
@@ -250,7 +259,7 @@ platformdirs = ">=2"
|
|||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
colorama = ["colorama (>=0.4.3)"]
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""]
|
||||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
@@ -260,6 +269,7 @@ version = "2024.7.4"
|
|||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||||
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||||
@@ -271,6 +281,7 @@ version = "8.1.7"
|
|||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["app", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||||
@@ -285,10 +296,12 @@ version = "0.4.6"
|
|||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
groups = ["app", "dev", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
markers = {app = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\"", test = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dateparser"
|
name = "dateparser"
|
||||||
@@ -296,6 +309,7 @@ version = "1.2.0"
|
|||||||
description = "Date parsing library designed to parse dates from HTML pages"
|
description = "Date parsing library designed to parse dates from HTML pages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"},
|
{file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"},
|
||||||
{file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"},
|
{file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"},
|
||||||
@@ -318,6 +332,7 @@ version = "0.3.8"
|
|||||||
description = "serialize all of Python"
|
description = "serialize all of Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"},
|
{file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"},
|
||||||
{file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"},
|
{file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"},
|
||||||
@@ -333,6 +348,7 @@ version = "2.6.1"
|
|||||||
description = "DNS toolkit"
|
description = "DNS toolkit"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
|
{file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
|
||||||
{file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
|
{file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
|
||||||
@@ -353,6 +369,7 @@ version = "2.2.0"
|
|||||||
description = "A robust email address syntax and deliverability validation library."
|
description = "A robust email address syntax and deliverability validation library."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
|
{file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
|
||||||
{file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"},
|
{file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"},
|
||||||
@@ -368,6 +385,7 @@ version = "0.111.1"
|
|||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi-0.111.1-py3-none-any.whl", hash = "sha256:4f51cfa25d72f9fbc3280832e84b32494cf186f50158d364a8765aabf22587bf"},
|
{file = "fastapi-0.111.1-py3-none-any.whl", hash = "sha256:4f51cfa25d72f9fbc3280832e84b32494cf186f50158d364a8765aabf22587bf"},
|
||||||
{file = "fastapi-0.111.1.tar.gz", hash = "sha256:ddd1ac34cb1f76c2e2d7f8545a4bcb5463bce4834e81abf0b189e0c359ab2413"},
|
{file = "fastapi-0.111.1.tar.gz", hash = "sha256:ddd1ac34cb1f76c2e2d7f8545a4bcb5463bce4834e81abf0b189e0c359ab2413"},
|
||||||
@@ -393,6 +411,7 @@ version = "0.0.4"
|
|||||||
description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀"
|
description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi_cli-0.0.4-py3-none-any.whl", hash = "sha256:a2552f3a7ae64058cdbb530be6fa6dbfc975dc165e4fa66d224c3d396e25e809"},
|
{file = "fastapi_cli-0.0.4-py3-none-any.whl", hash = "sha256:a2552f3a7ae64058cdbb530be6fa6dbfc975dc165e4fa66d224c3d396e25e809"},
|
||||||
{file = "fastapi_cli-0.0.4.tar.gz", hash = "sha256:e2e9ffaffc1f7767f488d6da34b6f5a377751c996f397902eb6abb99a67bde32"},
|
{file = "fastapi_cli-0.0.4.tar.gz", hash = "sha256:e2e9ffaffc1f7767f488d6da34b6f5a377751c996f397902eb6abb99a67bde32"},
|
||||||
@@ -410,6 +429,7 @@ version = "1.4.1"
|
|||||||
description = "A list-like structure which implements collections.abc.MutableSequence"
|
description = "A list-like structure which implements collections.abc.MutableSequence"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
|
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
|
||||||
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
|
{file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
|
||||||
@@ -496,6 +516,7 @@ version = "0.14.0"
|
|||||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||||
@@ -507,6 +528,7 @@ version = "1.0.5"
|
|||||||
description = "A minimal low-level HTTP client."
|
description = "A minimal low-level HTTP client."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
||||||
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
||||||
@@ -528,6 +550,7 @@ version = "0.6.1"
|
|||||||
description = "A collection of framework independent HTTP protocol utils."
|
description = "A collection of framework independent HTTP protocol utils."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.8.0"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"},
|
{file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"},
|
||||||
{file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"},
|
{file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"},
|
||||||
@@ -576,6 +599,7 @@ version = "0.27.0"
|
|||||||
description = "The next generation HTTP client."
|
description = "The next generation HTTP client."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
||||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
||||||
@@ -589,7 +613,7 @@ idna = "*"
|
|||||||
sniffio = "*"
|
sniffio = "*"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
brotli = ["brotli", "brotlicffi"]
|
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||||
http2 = ["h2 (>=3,<5)"]
|
http2 = ["h2 (>=3,<5)"]
|
||||||
socks = ["socksio (==1.*)"]
|
socks = ["socksio (==1.*)"]
|
||||||
@@ -600,6 +624,7 @@ version = "3.7"
|
|||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
groups = ["main", "app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||||
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||||
@@ -611,6 +636,7 @@ version = "2.0.0"
|
|||||||
description = "brain-dead simple config-ini parsing"
|
description = "brain-dead simple config-ini parsing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
@@ -622,6 +648,7 @@ version = "5.13.2"
|
|||||||
description = "A Python utility / library to sort Python imports."
|
description = "A Python utility / library to sort Python imports."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.8.0"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
|
||||||
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
|
||||||
@@ -636,6 +663,7 @@ version = "3.1.4"
|
|||||||
description = "A very fast and expressive template engine."
|
description = "A very fast and expressive template engine."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
||||||
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
||||||
@@ -653,6 +681,7 @@ version = "3.0.0"
|
|||||||
description = "Python port of markdown-it. Markdown parsing, done right!"
|
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
|
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
|
||||||
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
|
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
|
||||||
@@ -677,6 +706,7 @@ version = "2.1.5"
|
|||||||
description = "Safely add untrusted strings to HTML/XML markup."
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
|
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
|
||||||
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
||||||
@@ -746,6 +776,7 @@ version = "0.7.0"
|
|||||||
description = "McCabe checker, plugin for flake8"
|
description = "McCabe checker, plugin for flake8"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||||
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||||
@@ -757,6 +788,7 @@ version = "0.1.2"
|
|||||||
description = "Markdown URL utilities"
|
description = "Markdown URL utilities"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||||
@@ -768,6 +800,7 @@ version = "6.0.5"
|
|||||||
description = "multidict implementation"
|
description = "multidict implementation"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
|
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"},
|
||||||
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
|
{file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"},
|
||||||
@@ -867,6 +900,7 @@ version = "1.0.0"
|
|||||||
description = "Type system extensions for programs checked with the mypy type checker."
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||||
@@ -878,6 +912,7 @@ version = "24.1"
|
|||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev", "test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||||
@@ -889,6 +924,7 @@ version = "0.12.1"
|
|||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||||
@@ -900,6 +936,7 @@ version = "4.2.2"
|
|||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
|
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
|
||||||
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
|
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
|
||||||
@@ -916,6 +953,7 @@ version = "1.5.0"
|
|||||||
description = "plugin and hook calling mechanisms for python"
|
description = "plugin and hook calling mechanisms for python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||||
@@ -931,6 +969,7 @@ version = "2.8.2"
|
|||||||
description = "Data validation using Python type hints"
|
description = "Data validation using Python type hints"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main", "app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
|
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
|
||||||
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
|
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
|
||||||
@@ -953,6 +992,7 @@ version = "2.20.1"
|
|||||||
description = "Core functionality for Pydantic validation and serialization"
|
description = "Core functionality for Pydantic validation and serialization"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main", "app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
|
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
|
||||||
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
|
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
|
||||||
@@ -1054,6 +1094,7 @@ version = "2.18.0"
|
|||||||
description = "Pygments is a syntax highlighting package written in Python."
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
||||||
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
||||||
@@ -1068,6 +1109,7 @@ version = "3.2.6"
|
|||||||
description = "python code static checker"
|
description = "python code static checker"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.8.0"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"},
|
{file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"},
|
||||||
{file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"},
|
{file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"},
|
||||||
@@ -1092,6 +1134,7 @@ version = "8.3.1"
|
|||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"},
|
{file = "pytest-8.3.1-py3-none-any.whl", hash = "sha256:e9600ccf4f563976e2c99fa02c7624ab938296551f280835ee6516df8bc4ae8c"},
|
||||||
{file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"},
|
{file = "pytest-8.3.1.tar.gz", hash = "sha256:7e8e5c5abd6e93cb1cc151f23e57adc31fcf8cfd2a3ff2da63e23f732de35db6"},
|
||||||
@@ -1112,6 +1155,7 @@ version = "0.23.8"
|
|||||||
description = "Pytest support for asyncio"
|
description = "Pytest support for asyncio"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["test"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"},
|
{file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"},
|
||||||
{file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"},
|
{file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"},
|
||||||
@@ -1130,6 +1174,7 @@ version = "2.9.0.post0"
|
|||||||
description = "Extensions to the standard Python datetime module"
|
description = "Extensions to the standard Python datetime module"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||||
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||||
@@ -1144,6 +1189,7 @@ version = "1.0.1"
|
|||||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
|
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
|
||||||
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
|
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
|
||||||
@@ -1158,6 +1204,7 @@ version = "0.0.9"
|
|||||||
description = "A streaming multipart parser for Python"
|
description = "A streaming multipart parser for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"},
|
{file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"},
|
||||||
{file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"},
|
{file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"},
|
||||||
@@ -1172,6 +1219,7 @@ version = "2024.1"
|
|||||||
description = "World timezone definitions, modern and historical"
|
description = "World timezone definitions, modern and historical"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
|
{file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
|
||||||
{file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
|
{file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
|
||||||
@@ -1183,6 +1231,7 @@ version = "6.0.1"
|
|||||||
description = "YAML parser and emitter for Python"
|
description = "YAML parser and emitter for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
|
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
|
||||||
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
|
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
|
||||||
@@ -1243,6 +1292,7 @@ version = "5.0.8"
|
|||||||
description = "Python client for Redis database and key-value store"
|
description = "Python client for Redis database and key-value store"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"},
|
{file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"},
|
||||||
{file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"},
|
{file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"},
|
||||||
@@ -1258,6 +1308,7 @@ version = "2024.5.15"
|
|||||||
description = "Alternative regular expression module, to replace re."
|
description = "Alternative regular expression module, to replace re."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"},
|
{file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"},
|
||||||
{file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"},
|
{file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"},
|
||||||
@@ -1346,6 +1397,7 @@ version = "13.7.1"
|
|||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.0"
|
python-versions = ">=3.7.0"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
|
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
|
||||||
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
|
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
|
||||||
@@ -1364,6 +1416,7 @@ version = "1.5.4"
|
|||||||
description = "Tool to Detect Surrounding Shell"
|
description = "Tool to Detect Surrounding Shell"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
|
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
|
||||||
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
||||||
@@ -1375,6 +1428,7 @@ version = "1.16.0"
|
|||||||
description = "Python 2 and 3 compatibility utilities"
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
@@ -1386,6 +1440,7 @@ version = "1.3.1"
|
|||||||
description = "Sniff out which async library your code is running under"
|
description = "Sniff out which async library your code is running under"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||||
@@ -1397,6 +1452,7 @@ version = "2.5"
|
|||||||
description = "A modern CSS selector implementation for Beautiful Soup."
|
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
|
{file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
|
||||||
{file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
|
{file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
|
||||||
@@ -1408,6 +1464,7 @@ version = "0.37.2"
|
|||||||
description = "The little ASGI library that shines."
|
description = "The little ASGI library that shines."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"},
|
{file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"},
|
||||||
{file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"},
|
{file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"},
|
||||||
@@ -1425,6 +1482,7 @@ version = "0.13.0"
|
|||||||
description = "Style preserving TOML library"
|
description = "Style preserving TOML library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"},
|
{file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"},
|
||||||
{file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"},
|
{file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"},
|
||||||
@@ -1436,6 +1494,7 @@ version = "0.12.3"
|
|||||||
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"},
|
{file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"},
|
||||||
{file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"},
|
{file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"},
|
||||||
@@ -1453,6 +1512,7 @@ version = "4.12.2"
|
|||||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main", "app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||||
@@ -1464,6 +1524,8 @@ version = "2024.1"
|
|||||||
description = "Provider of IANA time zone data"
|
description = "Provider of IANA time zone data"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2"
|
python-versions = ">=2"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "platform_system == \"Windows\""
|
||||||
files = [
|
files = [
|
||||||
{file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
|
{file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
|
||||||
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
|
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
|
||||||
@@ -1475,6 +1537,7 @@ version = "5.2"
|
|||||||
description = "tzinfo object for the local timezone"
|
description = "tzinfo object for the local timezone"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"},
|
{file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"},
|
||||||
{file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"},
|
{file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"},
|
||||||
@@ -1492,6 +1555,7 @@ version = "0.30.3"
|
|||||||
description = "The lightning-fast ASGI server."
|
description = "The lightning-fast ASGI server."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "uvicorn-0.30.3-py3-none-any.whl", hash = "sha256:94a3608da0e530cea8f69683aa4126364ac18e3826b6630d1a65f4638aade503"},
|
{file = "uvicorn-0.30.3-py3-none-any.whl", hash = "sha256:94a3608da0e530cea8f69683aa4126364ac18e3826b6630d1a65f4638aade503"},
|
||||||
{file = "uvicorn-0.30.3.tar.gz", hash = "sha256:0d114d0831ff1adbf231d358cbf42f17333413042552a624ea6a9b4c33dcfd81"},
|
{file = "uvicorn-0.30.3.tar.gz", hash = "sha256:0d114d0831ff1adbf231d358cbf42f17333413042552a624ea6a9b4c33dcfd81"},
|
||||||
@@ -1504,12 +1568,12 @@ h11 = ">=0.8"
|
|||||||
httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""}
|
httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""}
|
||||||
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
|
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
|
||||||
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
|
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
|
||||||
uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
|
uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
|
||||||
watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
|
watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
|
||||||
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
|
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvloop"
|
name = "uvloop"
|
||||||
@@ -1517,6 +1581,8 @@ version = "0.19.0"
|
|||||||
description = "Fast implementation of asyncio event loop on top of libuv"
|
description = "Fast implementation of asyncio event loop on top of libuv"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
python-versions = ">=3.8.0"
|
||||||
|
groups = ["app"]
|
||||||
|
markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""
|
||||||
files = [
|
files = [
|
||||||
{file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"},
|
{file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"},
|
||||||
{file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"},
|
{file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"},
|
||||||
@@ -1553,7 +1619,7 @@ files = [
|
|||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"]
|
||||||
test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
|
test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0) ; python_version >= \"3.12\"", "aiohttp (>=3.8.1) ; python_version < \"3.12\"", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "watchfiles"
|
name = "watchfiles"
|
||||||
@@ -1561,6 +1627,7 @@ version = "0.22.0"
|
|||||||
description = "Simple, modern and high performance file watching and code reload in python."
|
description = "Simple, modern and high performance file watching and code reload in python."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "watchfiles-0.22.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:da1e0a8caebf17976e2ffd00fa15f258e14749db5e014660f53114b676e68538"},
|
{file = "watchfiles-0.22.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:da1e0a8caebf17976e2ffd00fa15f258e14749db5e014660f53114b676e68538"},
|
||||||
{file = "watchfiles-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61af9efa0733dc4ca462347becb82e8ef4945aba5135b1638bfc20fad64d4f0e"},
|
{file = "watchfiles-0.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61af9efa0733dc4ca462347becb82e8ef4945aba5135b1638bfc20fad64d4f0e"},
|
||||||
@@ -1648,6 +1715,7 @@ version = "12.0"
|
|||||||
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["app"]
|
||||||
files = [
|
files = [
|
||||||
{file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"},
|
{file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"},
|
||||||
{file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"},
|
{file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"},
|
||||||
@@ -1729,6 +1797,7 @@ version = "1.9.4"
|
|||||||
description = "Yet another URL library"
|
description = "Yet another URL library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
|
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"},
|
||||||
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
|
{file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"},
|
||||||
@@ -1827,6 +1896,6 @@ idna = ">=2.0"
|
|||||||
multidict = ">=4.0"
|
multidict = ">=4.0"
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.12"
|
python-versions = "^3.12"
|
||||||
content-hash = "7295e9ec7f7492017c5bbda489026f19bbf155f0ea82402d348b0aa4c03beaca"
|
content-hash = "7295e9ec7f7492017c5bbda489026f19bbf155f0ea82402d348b0aa4c03beaca"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "gallery"
|
name = "gallery"
|
||||||
version = "0.1.0"
|
version = "0.2.1"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["shmyga <shmyga.z@gmail.com>"]
|
authors = ["shmyga <shmyga.z@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -38,3 +38,5 @@ gallery = "gallery.main:run"
|
|||||||
addopts = "-p no:warnings"
|
addopts = "-p no:warnings"
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
[tool.poetry_bumpversion.file."gallery/version.py"]
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
set -e
|
set -e
|
||||||
cd "$(dirname $(dirname "$0"))" || exit
|
cd "$(dirname $(dirname "$0"))" || exit
|
||||||
|
|
||||||
docker build -t shmyga/gallery .
|
docker compose -f docker-compose-develop.yaml up --build --watch
|
||||||
45
scripts/docker-action
Executable file
45
scripts/docker-action
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname $(dirname "$0"))" || exit
|
||||||
|
|
||||||
|
. .env
|
||||||
|
|
||||||
|
build () {
|
||||||
|
echo "build: $1"
|
||||||
|
. "$1/.env"
|
||||||
|
for PROJECT in "${DOCKER_PROJECTS[@]}"; do
|
||||||
|
IFS=: read -r PROJECT_NAME PROJECT_TARGET <<< "$PROJECT"
|
||||||
|
ARGS=("build")
|
||||||
|
for ARG in ${DOCKER_ARGS[@]}; do
|
||||||
|
ARGS+=("--build-arg" "$ARG")
|
||||||
|
done
|
||||||
|
if [ -n "$PROJECT_TARGET" ]; then
|
||||||
|
ARGS+=("--target" "$PROJECT_TARGET")
|
||||||
|
fi
|
||||||
|
ARGS+=("-t" "$DOCKER_GROUP/$PROJECT_NAME" ".")
|
||||||
|
ARGS+=("-f" "$1/Dockerfile")
|
||||||
|
echo "${ARGS[@]}"
|
||||||
|
docker "${ARGS[@]}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
publish () {
|
||||||
|
echo "publish: $1"
|
||||||
|
. "$1/.env"
|
||||||
|
for PROJECT in "${DOCKER_PROJECTS[@]}"; do
|
||||||
|
IFS=: read -r PROJECT_NAME PROJECT_TARGET <<< "$PROJECT"
|
||||||
|
docker tag $DOCKER_GROUP/$PROJECT_NAME $DOCKER_ROOT/$PROJECT_NAME:$VERSION
|
||||||
|
docker tag $DOCKER_GROUP/$PROJECT_NAME $DOCKER_ROOT/$PROJECT_NAME:latest
|
||||||
|
docker push $DOCKER_ROOT/$PROJECT_NAME:$VERSION
|
||||||
|
docker push $DOCKER_ROOT/$PROJECT_NAME:latest
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_TARGETS="."
|
||||||
|
TARGETS="${@-$DEFAULT_TARGETS}"
|
||||||
|
|
||||||
|
DOCKER_ACTION="${DOCKER_ACTION-build}"
|
||||||
|
|
||||||
|
for TARGET in $TARGETS; do
|
||||||
|
$DOCKER_ACTION "$TARGET"
|
||||||
|
done
|
||||||
6
scripts/locales
Executable file
6
scripts/locales
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname $(dirname "$0"))" || exit
|
||||||
|
|
||||||
|
cd gallery/easel/route/view/locales/ru/LC_MESSAGES || exit
|
||||||
|
msgfmt messages.po
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
cd "$(dirname $(dirname "$0"))" || exit
|
|
||||||
|
|
||||||
IMAGE_NAME=shmyga/gallery
|
|
||||||
|
|
||||||
docker tag $IMAGE_NAME instreamatic.com:8083/$IMAGE_NAME
|
|
||||||
docker push instreamatic.com:8083/$IMAGE_NAME
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
cd "$(dirname $(dirname "$0"))" || exit
|
|
||||||
|
|
||||||
# docker run --rm -p 8000:80 shmyga/gallery
|
|
||||||
docker compose up --build
|
|
||||||
19
scripts/version
Executable file
19
scripts/version
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname $(dirname "$0"))" || exit
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Usage: $0 [version]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -z "$(git status -s)" ]]; then
|
||||||
|
echo "Uncomitted changes"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
poetry version $1
|
||||||
|
(cd static && npm version $1 --allow-same-version)
|
||||||
|
git add .
|
||||||
|
git commit -m "ci(version): $1"
|
||||||
|
git tag $1
|
||||||
2
static/.dockerignore
Normal file
2
static/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
1
static/.nvmrc
Normal file
1
static/.nvmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
24
|
||||||
1234
static/package-lock.json
generated
Normal file
1234
static/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
static/package.json
Normal file
20
static/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "gallery",
|
||||||
|
"version": "0.2.1",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"dev": "vite build --watch"
|
||||||
|
},
|
||||||
|
"author": "shmyga <shmyga.z@gmail.com>",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"bootstrap": "^5.3.8",
|
||||||
|
"bootstrap-icons": "^1.13.1",
|
||||||
|
"sass": "^1.99.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^8.0.9"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
static/src/components.ts
Normal file
17
static/src/components.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class AppLinkElement extends HTMLElement {
|
||||||
|
static observedAttributes = ["icon", "href"];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.innerHTML = `
|
||||||
|
<a href="${this.getAttribute("href")}"
|
||||||
|
class="d-flex align-items-center text-body text-decoration-none">
|
||||||
|
<span class="fs-4">
|
||||||
|
<i class="bi bi-${this.getAttribute("icon")}"></i>
|
||||||
|
<span>${this.textContent}</span>
|
||||||
|
</span>
|
||||||
|
</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("app-link", AppLinkElement);
|
||||||
5
static/src/index.ts
Normal file
5
static/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import "./main.scss";
|
||||||
|
import "bootstrap";
|
||||||
|
import "./theme";
|
||||||
|
import "./language";
|
||||||
|
import "./components";
|
||||||
62
static/src/language.ts
Normal file
62
static/src/language.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
(() => {
|
||||||
|
const getStoredLanguage = () => {
|
||||||
|
const m = document.cookie.match(/language=(\w+)/);
|
||||||
|
return m ? m[1] : null;
|
||||||
|
};
|
||||||
|
const setStoredLanguage = (language: string) => (document.cookie = `language=${language}; max-age=34560000; path=/`);
|
||||||
|
|
||||||
|
const getPreferredLanguage = () => {
|
||||||
|
const storedLanguage = getStoredLanguage();
|
||||||
|
if (storedLanguage) {
|
||||||
|
return storedLanguage;
|
||||||
|
}
|
||||||
|
const result = window.navigator.language.split("-")[0];
|
||||||
|
return ["en", "ru"].includes(result) ? result : "en";
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLanguage = (language: string) => {};
|
||||||
|
|
||||||
|
setLanguage(getPreferredLanguage());
|
||||||
|
|
||||||
|
const showActiveLanguage = (language: string, focus = false) => {
|
||||||
|
const languageSwitcher = document.querySelector("#bd-language");
|
||||||
|
|
||||||
|
if (!languageSwitcher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageSwitcherText = document.querySelector("#bd-language-text");
|
||||||
|
const activeLanguageIcon = document.querySelector(".language-icon-active");
|
||||||
|
const btnToActive = document.querySelector(`[data-bs-language-value="${language}"]`);
|
||||||
|
const activeLanguageIconContent = btnToActive?.querySelector("span")?.textContent;
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-bs-language-value]").forEach((element) => {
|
||||||
|
element.classList.remove("active");
|
||||||
|
element.setAttribute("aria-pressed", "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
btnToActive.classList.add("active");
|
||||||
|
btnToActive.setAttribute("aria-pressed", "true");
|
||||||
|
activeLanguageIcon.textContent = activeLanguageIconContent;
|
||||||
|
const languageSwitcherLabel = `${languageSwitcherText.textContent} (${btnToActive.dataset.bsLanguageValue})`;
|
||||||
|
languageSwitcher.setAttribute("aria-label", languageSwitcherLabel);
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
languageSwitcher.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
showActiveLanguage(getPreferredLanguage());
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-bs-language-value]").forEach((toggle) => {
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
const language = toggle.getAttribute("data-bs-language-value") || "";
|
||||||
|
setStoredLanguage(language);
|
||||||
|
setLanguage(language);
|
||||||
|
showActiveLanguage(language, true);
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
19
static/src/main.scss
Normal file
19
static/src/main.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@import "bootstrap/scss/bootstrap";
|
||||||
|
$bootstrap-icons-font-dir: "bootstrap-icons/font/fonts";
|
||||||
|
@import "bootstrap-icons/font/bootstrap-icons";
|
||||||
|
|
||||||
|
@import "./widget.scss";
|
||||||
|
@import "./weather.scss";
|
||||||
|
|
||||||
|
.table.table-compact {
|
||||||
|
td {
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
76
static/src/theme.ts
Normal file
76
static/src/theme.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
(() => {
|
||||||
|
const getStoredTheme = () => localStorage.getItem("theme");
|
||||||
|
const setStoredTheme = (theme: string) => localStorage.setItem("theme", theme);
|
||||||
|
|
||||||
|
const getPreferredTheme = () => {
|
||||||
|
const storedTheme = getStoredTheme();
|
||||||
|
if (storedTheme) {
|
||||||
|
return storedTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTheme = (theme: string) => {
|
||||||
|
if (theme === "auto") {
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-bs-theme",
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute("data-bs-theme", theme);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTheme(getPreferredTheme());
|
||||||
|
|
||||||
|
const showActiveTheme = (theme: string, focus = false) => {
|
||||||
|
const themeSwitcher = document.querySelector("#bd-theme");
|
||||||
|
|
||||||
|
if (!themeSwitcher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSwitcherText = document.querySelector("#bd-theme-text");
|
||||||
|
const activeThemeIcon = document.querySelector(".theme-icon-active");
|
||||||
|
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`);
|
||||||
|
const activeThemeIconClass = btnToActive.querySelector("i.bi").className.match(/bi-[\w-]+/)[0];
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-bs-theme-value]").forEach((element) => {
|
||||||
|
element.classList.remove("active");
|
||||||
|
element.setAttribute("aria-pressed", "false");
|
||||||
|
});
|
||||||
|
|
||||||
|
btnToActive.classList.add("active");
|
||||||
|
btnToActive.setAttribute("aria-pressed", "true");
|
||||||
|
const classesToRemove = Array.from(activeThemeIcon.classList).filter((className) => className.startsWith("bi-"));
|
||||||
|
activeThemeIcon.classList.remove(...classesToRemove);
|
||||||
|
activeThemeIcon.classList.add(activeThemeIconClass);
|
||||||
|
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`;
|
||||||
|
themeSwitcher.setAttribute("aria-label", themeSwitcherLabel);
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
themeSwitcher.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
||||||
|
const storedTheme = getStoredTheme();
|
||||||
|
if (storedTheme !== "light" && storedTheme !== "dark") {
|
||||||
|
setTheme(getPreferredTheme());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
showActiveTheme(getPreferredTheme());
|
||||||
|
|
||||||
|
document.querySelectorAll("[data-bs-theme-value]").forEach((toggle) => {
|
||||||
|
toggle.addEventListener("click", () => {
|
||||||
|
const theme = toggle.getAttribute("data-bs-theme-value") || '';
|
||||||
|
setStoredTheme(theme);
|
||||||
|
setTheme(theme);
|
||||||
|
showActiveTheme(theme, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
73
static/src/weather.scss
Normal file
73
static/src/weather.scss
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
.table-weather {
|
||||||
|
.header {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: left;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
background: rgba(1, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date.now {
|
||||||
|
background: rgba(0, 128, 255, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date .value a {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloudness {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloudness .icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloudness .icon:first-child {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature .value {
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature .value.positive {
|
||||||
|
color: orangered;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temperature .value.negative {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wind .direction {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wind .gust {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.precipitation .value {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pressure {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pressure .value {
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
color: blueviolet;
|
||||||
|
}
|
||||||
|
|
||||||
|
.humidity .value {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
static/src/widget.scss
Normal file
17
static/src/widget.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.widget .app {
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget header {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget footer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
14
static/vite.config.ts
Normal file
14
static/vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import packageJson from "./package.json";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: "./",
|
||||||
|
build: {
|
||||||
|
outDir: "./dist",
|
||||||
|
lib: {
|
||||||
|
entry: "./src/index.ts",
|
||||||
|
name: packageJson.name,
|
||||||
|
fileName: (format) => `${packageJson.name}.${format}.js`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
0
tests/common/__init__.py
Normal file
0
tests/common/__init__.py
Normal file
17
tests/common/mock.py
Normal file
17
tests/common/mock.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from gallery.sketch.source import ApiSource
|
||||||
|
|
||||||
|
|
||||||
|
class MockSource(ApiSource):
|
||||||
|
|
||||||
|
def __init__(self, path: Path, mapping: dict[str, str]):
|
||||||
|
super().__init__("")
|
||||||
|
self._path = path
|
||||||
|
self._mapping = mapping
|
||||||
|
|
||||||
|
async def request(self, endpoint: str) -> str:
|
||||||
|
for pattern, filename in self._mapping.items():
|
||||||
|
if pattern in endpoint:
|
||||||
|
return (self._path / filename).read_text()
|
||||||
|
raise ValueError(endpoint)
|
||||||
6334
tests/data/gismeteo/10-days.html
Normal file
6334
tests/data/gismeteo/10-days.html
Normal file
File diff suppressed because one or more lines are too long
12
tests/data/gismeteo/__init__.py
Normal file
12
tests/data/gismeteo/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tests.common.mock import MockSource
|
||||||
|
|
||||||
|
GISMETEO_MOCK_SOURCE = MockSource(
|
||||||
|
Path(__file__).parent,
|
||||||
|
{
|
||||||
|
"today": "today.html",
|
||||||
|
"10-days": "10-days.html",
|
||||||
|
"mq/city/q": "mq_city_q.json",
|
||||||
|
},
|
||||||
|
)
|
||||||
400
tests/data/gismeteo/mq_city_q.json
Normal file
400
tests/data/gismeteo/mq_city_q.json
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
{
|
||||||
|
"meta": { "status": true },
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 4432,
|
||||||
|
"kind": "M",
|
||||||
|
"slug": "orel",
|
||||||
|
"coordinates": { "latitude": 52.968498, "longitude": 36.0695 },
|
||||||
|
"obsStationId": 11948,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 156, "slug": "russia", "code": "RU" },
|
||||||
|
"district": { "id": 253, "slug": "oryol-oblast" },
|
||||||
|
"subdistrict": { "id": 4728, "slug": "urban-district-city-oryol" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Россия", "nameP": "в России", "nameR": "России" },
|
||||||
|
"district": { "name": "Орловская область", "nameP": "в Орловской области", "nameR": "Орловской области" },
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "городской округ город Орёл",
|
||||||
|
"nameP": "в городском округе города Орёл",
|
||||||
|
"nameR": "городского округа города Орёл"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Россия", "nameP": "в России", "nameR": "России" },
|
||||||
|
"district": { "name": "Орловская область", "nameP": "в Орловской области", "nameR": "Орловской области" },
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "городской округ город Орёл",
|
||||||
|
"nameP": "в городском округе города Орёл",
|
||||||
|
"nameR": "городского округа города Орёл"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13074,
|
||||||
|
"kind": "A",
|
||||||
|
"slug": "orel-yuzhnyy-im-i-s-turgeneva",
|
||||||
|
"coordinates": { "latitude": 52.935001, "longitude": 36.001671 },
|
||||||
|
"obsStationId": 11948,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 156, "slug": "russia", "code": "RU" },
|
||||||
|
"district": { "id": 253, "slug": "oryol-oblast" },
|
||||||
|
"subdistrict": { "id": 4728, "slug": "urban-district-city-oryol" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орел / Южный им. И. С. Тургенева", "nameP": "Орел / Южный им. И. С. Тургенева" },
|
||||||
|
"country": { "name": "Россия", "nameP": "в России", "nameR": "России" },
|
||||||
|
"district": { "name": "Орловская область", "nameP": "в Орловской области", "nameR": "Орловской области" },
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "городской округ город Орёл",
|
||||||
|
"nameP": "в городском округе города Орёл",
|
||||||
|
"nameR": "городского округа города Орёл"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орел / Южный им. И. С. Тургенева", "nameP": "Орел / Южный им. И. С. Тургенева" },
|
||||||
|
"country": { "name": "Россия", "nameP": "в России", "nameR": "России" },
|
||||||
|
"district": { "name": "Орловская область", "nameP": "в Орловской области", "nameR": "Орловской области" },
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "городской округ город Орёл",
|
||||||
|
"nameP": "в городском округе города Орёл",
|
||||||
|
"nameR": "городского округа города Орёл"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 112316,
|
||||||
|
"kind": "T",
|
||||||
|
"slug": "orel",
|
||||||
|
"coordinates": { "latitude": 52.0172, "longitude": 30.849199 },
|
||||||
|
"obsStationId": 12921,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 19, "slug": "belarus", "code": "BY" },
|
||||||
|
"district": { "id": 346, "slug": "gomel-region" },
|
||||||
|
"subdistrict": { "id": 1828, "slug": "loyev-district" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Беларусь", "nameP": "в Беларуси", "nameR": "Беларуси" },
|
||||||
|
"district": { "name": "Гомельская область", "nameP": "в Гомельской области", "nameR": "Гомельской области" },
|
||||||
|
"subdistrict": { "name": "Лоевский район", "nameP": "в Лоевском районе", "nameR": "Лоевского района" }
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Беларусь", "nameP": "в Беларуси", "nameR": "Беларуси" },
|
||||||
|
"district": { "name": "Гомельская область", "nameP": "в Гомельской области", "nameR": "Гомельской области" },
|
||||||
|
"subdistrict": { "name": "Лоевский район", "nameP": "в Лоевском районе", "nameR": "Лоевского района" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 178290,
|
||||||
|
"kind": "T",
|
||||||
|
"slug": "orel",
|
||||||
|
"coordinates": { "latitude": 58.799999, "longitude": 34.453701 },
|
||||||
|
"obsStationId": 11657,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 156, "slug": "russia", "code": "RU" },
|
||||||
|
"district": { "id": 248, "slug": "novgorod-oblast" },
|
||||||
|
"subdistrict": { "id": 2857, "slug": "municipal-district-khvoyninsky" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Россия", "nameP": "в России", "nameR": "России" },
|
||||||
|
"district": {
|
||||||
|
"name": "Новгородская область",
|
||||||
|
"nameP": "в Новгородской области",
|
||||||
|
"nameR": "Новгородской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "муниципальный округ Хвойнинский",
|
||||||
|
"nameP": "в муниципальном округе Хвойнинском",
|
||||||
|
"nameR": "муниципального округа Хвойнинского"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Россия", "nameP": "в России", "nameR": "России" },
|
||||||
|
"district": {
|
||||||
|
"name": "Новгородская область",
|
||||||
|
"nameP": "в Новгородской области",
|
||||||
|
"nameR": "Новгородской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "муниципальный округ Хвойнинский",
|
||||||
|
"nameP": "в муниципальном округе Хвойнинском",
|
||||||
|
"nameR": "муниципального округа Хвойнинского"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 112830,
|
||||||
|
"kind": "T",
|
||||||
|
"slug": "orel",
|
||||||
|
"coordinates": { "latitude": 52.182499, "longitude": 30.4349 },
|
||||||
|
"obsStationId": 12920,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 19, "slug": "belarus", "code": "BY" },
|
||||||
|
"district": { "id": 346, "slug": "gomel-region" },
|
||||||
|
"subdistrict": { "id": 1833, "slug": "rechytsa-district" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Беларусь", "nameP": "в Беларуси", "nameR": "Беларуси" },
|
||||||
|
"district": { "name": "Гомельская область", "nameP": "в Гомельской области", "nameR": "Гомельской области" },
|
||||||
|
"subdistrict": { "name": "Речицкий район", "nameP": "в Речицком районе", "nameR": "Речицкого района" }
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Беларусь", "nameP": "в Беларуси", "nameR": "Беларуси" },
|
||||||
|
"district": { "name": "Гомельская область", "nameP": "в Гомельской области", "nameR": "Гомельской области" },
|
||||||
|
"subdistrict": { "name": "Речицкий район", "nameP": "в Речицком районе", "nameR": "Речицкого района" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97816,
|
||||||
|
"kind": "T",
|
||||||
|
"slug": "orilske",
|
||||||
|
"coordinates": { "latitude": 49.088799, "longitude": 36.228401 },
|
||||||
|
"obsStationId": 13147,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 198, "slug": "ukraine", "code": "UA" },
|
||||||
|
"district": { "id": 335, "slug": "kharkiv-oblast" },
|
||||||
|
"subdistrict": { "id": 1646, "slug": "berestyn-district" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орельское", "nameP": "в Орельском" },
|
||||||
|
"country": { "name": "Украина", "nameP": "на Украине", "nameR": "Украины" },
|
||||||
|
"district": {
|
||||||
|
"name": "Харьковская область",
|
||||||
|
"nameP": "в Харьковской области",
|
||||||
|
"nameR": "Харьковской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "Берестинский район",
|
||||||
|
"nameP": "в Берестинском районе",
|
||||||
|
"nameR": "Берестинского района"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орельское", "nameP": "в Орельском" },
|
||||||
|
"country": { "name": "Украина", "nameP": "на Украине", "nameR": "Украины" },
|
||||||
|
"district": {
|
||||||
|
"name": "Харьковская область",
|
||||||
|
"nameP": "в Харьковской области",
|
||||||
|
"nameR": "Харьковской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "Берестинский район",
|
||||||
|
"nameP": "в Берестинском районе",
|
||||||
|
"nameR": "Берестинского района"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97619,
|
||||||
|
"kind": "T",
|
||||||
|
"slug": "orilka",
|
||||||
|
"coordinates": { "latitude": 48.980499, "longitude": 36.0075 },
|
||||||
|
"obsStationId": 13147,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 198, "slug": "ukraine", "code": "UA" },
|
||||||
|
"district": { "id": 335, "slug": "kharkiv-oblast" },
|
||||||
|
"subdistrict": { "id": 1649, "slug": "lozivskyi-district" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орелька", "nameP": "в Орельке" },
|
||||||
|
"country": { "name": "Украина", "nameP": "на Украине", "nameR": "Украины" },
|
||||||
|
"district": {
|
||||||
|
"name": "Харьковская область",
|
||||||
|
"nameP": "в Харьковской области",
|
||||||
|
"nameR": "Харьковской области"
|
||||||
|
},
|
||||||
|
"subdistrict": { "name": "Лозовский район", "nameP": "в Лозовском районе", "nameR": "Лозовского района" }
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орелька", "nameP": "в Орельке" },
|
||||||
|
"country": { "name": "Украина", "nameP": "на Украине", "nameR": "Украины" },
|
||||||
|
"district": {
|
||||||
|
"name": "Харьковская область",
|
||||||
|
"nameP": "в Харьковской области",
|
||||||
|
"nameR": "Харьковской области"
|
||||||
|
},
|
||||||
|
"subdistrict": { "name": "Лозовский район", "nameP": "в Лозовском районе", "nameR": "Лозовского района" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78141,
|
||||||
|
"kind": "T",
|
||||||
|
"slug": "orilka",
|
||||||
|
"coordinates": { "latitude": 48.945999, "longitude": 35.689098 },
|
||||||
|
"obsStationId": 13158,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 198, "slug": "ukraine", "code": "UA" },
|
||||||
|
"district": { "id": 319, "slug": "dnipropetrovsk-oblast" },
|
||||||
|
"subdistrict": { "id": 1184, "slug": "samarivskyi-district" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орелька", "nameP": "в Орельке" },
|
||||||
|
"country": { "name": "Украина", "nameP": "на Украине", "nameR": "Украины" },
|
||||||
|
"district": {
|
||||||
|
"name": "Днепропетровская область",
|
||||||
|
"nameP": "в Днепропетровской области",
|
||||||
|
"nameR": "Днепропетровской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "Самаровский район",
|
||||||
|
"nameP": "в Самаровском районе",
|
||||||
|
"nameR": "Самаровского района"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орелька", "nameP": "в Орельке" },
|
||||||
|
"country": { "name": "Украина", "nameP": "на Украине", "nameR": "Украины" },
|
||||||
|
"district": {
|
||||||
|
"name": "Днепропетровская область",
|
||||||
|
"nameP": "в Днепропетровской области",
|
||||||
|
"nameR": "Днепропетровской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "Самаровский район",
|
||||||
|
"nameP": "в Самаровском районе",
|
||||||
|
"nameR": "Самаровского района"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77735,
|
||||||
|
"kind": "T",
|
||||||
|
"slug": "orilske",
|
||||||
|
"coordinates": { "latitude": 48.587799, "longitude": 34.8111 },
|
||||||
|
"obsStationId": 13158,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 198, "slug": "ukraine", "code": "UA" },
|
||||||
|
"district": { "id": 319, "slug": "dnipropetrovsk-oblast" },
|
||||||
|
"subdistrict": { "id": 1178, "slug": "dniprovskyi-district" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орельское (Партизанское)", "nameP": "в Орельском (Партизанском)" },
|
||||||
|
"country": { "name": "Украина", "nameP": "на Украине", "nameR": "Украины" },
|
||||||
|
"district": {
|
||||||
|
"name": "Днепропетровская область",
|
||||||
|
"nameP": "в Днепропетровской области",
|
||||||
|
"nameR": "Днепропетровской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "Днепровский район",
|
||||||
|
"nameP": "в Днепровском районе",
|
||||||
|
"nameR": "Днепровского района"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орельское (Партизанское)", "nameP": "в Орельском (Партизанском)" },
|
||||||
|
"country": { "name": "Украина", "nameP": "на Украине", "nameR": "Украины" },
|
||||||
|
"district": {
|
||||||
|
"name": "Днепропетровская область",
|
||||||
|
"nameP": "в Днепропетровской области",
|
||||||
|
"nameR": "Днепропетровской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "Днепровский район",
|
||||||
|
"nameP": "в Днепровском районе",
|
||||||
|
"nameR": "Днепровского района"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 171956,
|
||||||
|
"kind": "T",
|
||||||
|
"slug": "orel",
|
||||||
|
"coordinates": { "latitude": 55.516499, "longitude": 44.0658 },
|
||||||
|
"obsStationId": 11899,
|
||||||
|
"timeZone": 180,
|
||||||
|
"country": { "id": 156, "slug": "russia", "code": "RU" },
|
||||||
|
"district": { "id": 266, "slug": "nizhny-novgorod-oblast" },
|
||||||
|
"subdistrict": { "id": 2796, "slug": "municipal-district-vadsky" },
|
||||||
|
"translations": {
|
||||||
|
"ru": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Россия", "nameP": "в России", "nameR": "России" },
|
||||||
|
"district": {
|
||||||
|
"name": "Нижегородская область",
|
||||||
|
"nameP": "в Нижегородской области",
|
||||||
|
"nameR": "Нижегородской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "муниципальный округ Вадский",
|
||||||
|
"nameP": "в муниципальном округе Вадском",
|
||||||
|
"nameR": "муниципального округа Вадского"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kk": {
|
||||||
|
"city": { "name": "Орел", "nameP": "в Орле" },
|
||||||
|
"country": { "name": "Россия", "nameP": "в России", "nameR": "России" },
|
||||||
|
"district": {
|
||||||
|
"name": "Нижегородская область",
|
||||||
|
"nameP": "в Нижегородской области",
|
||||||
|
"nameR": "Нижегородской области"
|
||||||
|
},
|
||||||
|
"subdistrict": {
|
||||||
|
"name": "муниципальный округ Вадский",
|
||||||
|
"nameP": "в муниципальном округе Вадском",
|
||||||
|
"nameR": "муниципального округа Вадского"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitCount": 0,
|
||||||
|
"options": { "altitude": 0, "mrlExists": false, "significantHeightDiff": false, "landSeaMask": 0 },
|
||||||
|
"meta": { "nowcast": true, "allergy": { "birch": true, "grass": true, "ragweed": true } },
|
||||||
|
"redirectUrl": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
6114
tests/data/gismeteo/today.html
Normal file
6114
tests/data/gismeteo/today.html
Normal file
File diff suppressed because one or more lines are too long
10
tests/data/matchtv/__init__.py
Normal file
10
tests/data/matchtv/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tests.common.mock import MockSource
|
||||||
|
|
||||||
|
MATCHTV_MOCK_SOURCE = MockSource(
|
||||||
|
Path(__file__).parent,
|
||||||
|
{
|
||||||
|
"test": "test.html",
|
||||||
|
},
|
||||||
|
)
|
||||||
2
tests/data/matchtv/test.html
Normal file
2
tests/data/matchtv/test.html
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user