Compare commits
10 Commits
ad8144df37
...
0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c3b3aeafc | |||
| d1592150fd | |||
| 9351b9f53a | |||
| ecb574e286 | |||
| 94870a5c86 | |||
| 3dd0a5410c | |||
| a0e6f30e3b | |||
| 29fa6435ce | |||
| a886322d0e | |||
| 6112147b40 |
@@ -6,6 +6,7 @@ indent_style = space
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
max_line_length = 120
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
max_line_length = off
|
max_line_length = off
|
||||||
|
|||||||
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:
|
||||||
|
|||||||
@@ -33,12 +33,14 @@
|
|||||||
"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,10 +1,13 @@
|
|||||||
import locale as _locale
|
import locale as _locale
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from gallery.sketch.bundle import ApiBundle
|
from gallery.sketch.bundle import ApiBundle
|
||||||
|
from gallery.util import root_path
|
||||||
|
|
||||||
from .route import api, doc, view
|
from .route import api, doc
|
||||||
|
from .route.view import router as view_router
|
||||||
|
|
||||||
DEFAULT_LOCALE = "ru_RU.UTF-8"
|
DEFAULT_LOCALE = "ru_RU.UTF-8"
|
||||||
|
|
||||||
@@ -17,7 +20,8 @@ def build_app(api_bundle: ApiBundle, *, locale: str = DEFAULT_LOCALE) -> FastAPI
|
|||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
)
|
)
|
||||||
app.state.api = api_bundle
|
app.state.api = api_bundle
|
||||||
|
app.mount("/static", StaticFiles(directory=root_path / "static/dist"))
|
||||||
doc.mount(app)
|
doc.mount(app)
|
||||||
api.mount(app)
|
api.mount(app)
|
||||||
view.mount(app)
|
app.include_router(view_router)
|
||||||
return app
|
return 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)
|
||||||
|
|||||||
@@ -3,23 +3,25 @@ import datetime
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from gallery.easel.core import AppRequest
|
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: AppRequest) -> list[str]:
|
async def get_api_weather_locations(
|
||||||
|
request: AppRequest, query: str
|
||||||
|
) -> list[Location]:
|
||||||
weather_api = request.app.state.api.weather
|
weather_api = request.app.state.api.weather
|
||||||
return await weather_api.get_locations()
|
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: AppRequest, location: str, date: datetime.date
|
request: AppRequest, location: str, date: datetime.date
|
||||||
) -> WeatherResponse:
|
) -> WeatherResponse:
|
||||||
weather_api = request.app.state.api.weather
|
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: AppRequest, location: str, days: int
|
request: AppRequest, location: str, days: int
|
||||||
) -> WeatherResponse:
|
) -> WeatherResponse:
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from . import common, schedule, weather
|
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)])
|
||||||
def mount(app: FastAPI):
|
router.include_router(common_router)
|
||||||
common.mount(app)
|
router.include_router(weather_router)
|
||||||
weather.mount(app)
|
router.include_router(schedule_router)
|
||||||
schedule.mount(app)
|
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
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({"_": _})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_class=HTMLResponse)
|
||||||
|
async def get_section_list(request: Request):
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="root_index.html",
|
name="root_index.html",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,112 +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: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-menu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-content {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 0.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-link-home > * {
|
|
||||||
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;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{{request.state.language}}">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
@@ -10,29 +10,115 @@
|
|||||||
content="ie=edge">
|
content="ie=edge">
|
||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="/static/common/style.css?v={{version}}">
|
href="/static/gallery.css?v={{version}}">
|
||||||
|
<script type="module"
|
||||||
|
src="/static/gallery.es.js?v={{version}}"></script>
|
||||||
<link rel="icon"
|
<link rel="icon"
|
||||||
href="/static/common/favicon.ico?v={{version}}"
|
href="/favicon.ico?v={{version}}"
|
||||||
type="image/x-icon">
|
type="image/x-icon">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="app-container">
|
<body>
|
||||||
<div class="app-menu">
|
<div class="app col-lg-8 mx-auto p-3 py-md-5">
|
||||||
<a class="app-link-home"
|
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
|
||||||
href="/">
|
<app-link href="/"
|
||||||
<div></div>
|
icon="gear">API Gallery</app-link>
|
||||||
</a>
|
{% block header %}{% endblock %}
|
||||||
</div>
|
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
|
||||||
<div class="app-content">
|
<li class="nav-item dropdown">
|
||||||
<h3 class="app-header">
|
<button class="btn btn-link nav-link py-2 px-0 px-lg-2 dropdown-toggle d-flex align-items-center"
|
||||||
{% block header %}{% endblock %}</span>
|
id="bd-language"
|
||||||
</h3>
|
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 %}
|
{% block content %}{% endblock %}
|
||||||
<div class="app-footer">
|
</main>
|
||||||
{% block footer %}{% endblock %}
|
<footer class="pt-5 my-5 text-muted border-top">
|
||||||
</div>
|
Created by shmyga · © 2026
|
||||||
|
</footer>
|
||||||
</div>
|
</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>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,21 +1,26 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Информация{% endblock %}
|
{% block title %}Index{% endblock %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}Информация{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul class="app-list">
|
<h1>View</h1>
|
||||||
|
<div class="list-group mb-5">
|
||||||
{% for section in sections %}
|
{% for section in sections %}
|
||||||
<li>
|
<a href="{{section.link}}"
|
||||||
<a href="{{section.link}}">
|
class="list-group-item list-group-item-action px-4">
|
||||||
<span class="icon"
|
<app-link href="{{section.link}}"
|
||||||
style="background-image: url(/static/{{section.link}}/{{section.link}}.png);"></span>
|
icon="{{section.icon}}">
|
||||||
<span>{{section.title}}</span>
|
{{_(section.title)}}
|
||||||
|
</app-link>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</div>
|
||||||
|
<hr class="col-3 col-md-2 mb-5">
|
||||||
|
<h1>Docs</h1>
|
||||||
|
<a href="/docs"
|
||||||
|
target="_blank">
|
||||||
|
<h4>Swagger</h4>
|
||||||
|
</a>
|
||||||
{% endblock %}
|
{% 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,9 +1,8 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI
|
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.easel.core import AppRequest
|
from gallery.easel.core import AppRequest
|
||||||
@@ -11,22 +10,24 @@ 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
|
||||||
def mount(app: FastAPI):
|
templates = Jinja2Templates(
|
||||||
base_dir = Path(__file__).parent
|
|
||||||
app.mount("/static/schedule", StaticFiles(directory=base_dir / "static"))
|
|
||||||
templates = Jinja2Templates(
|
|
||||||
directory=[
|
directory=[
|
||||||
base_dir.parent / "common/templates",
|
base_dir.parent / "common/templates",
|
||||||
base_dir / "templates",
|
base_dir / "templates",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
templates.env.filters["timedelta_format"] = timedelta_format
|
templates.env.globals.update({"_": _})
|
||||||
|
templates.env.filters["timedelta_format"] = timedelta_format
|
||||||
|
|
||||||
@app.get("/schedule", response_class=HTMLResponse)
|
router = APIRouter()
|
||||||
async def get_schedule_list(request: AppRequest):
|
|
||||||
|
|
||||||
|
@router.get("/schedule", response_class=HTMLResponse)
|
||||||
|
async def get_schedule_list(request: AppRequest):
|
||||||
schedule_api = request.app.state.api.schedule
|
schedule_api = request.app.state.api.schedule
|
||||||
channels = await schedule_api.get_channels()
|
channels = await schedule_api.get_channels()
|
||||||
channels_data = BUNDLE.select_items(channels)
|
channels_data = BUNDLE.select_items(channels)
|
||||||
@@ -39,15 +40,12 @@ def mount(app: FastAPI):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/schedule/tag/{tag}", response_class=HTMLResponse)
|
|
||||||
async def get_schedule_tag(request: AppRequest, tag: str, live: bool = False):
|
@router.get("/schedule/tag/{tag}", response_class=HTMLResponse)
|
||||||
|
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 = request.app.state.api.schedule
|
schedule_api = request.app.state.api.schedule
|
||||||
channels = await schedule_api.get_channels()
|
results = await schedule_api.get_all_schedules(tag_value.date)
|
||||||
responses = [
|
|
||||||
await schedule_api.get_channel_schedule(channel, tag_value.date)
|
|
||||||
for channel in channels
|
|
||||||
]
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="schedule.html",
|
name="schedule.html",
|
||||||
@@ -55,19 +53,20 @@ def mount(app: FastAPI):
|
|||||||
"version": __version__,
|
"version": __version__,
|
||||||
"tag_util": TagUtil,
|
"tag_util": TagUtil,
|
||||||
"datetime": datetime,
|
"datetime": datetime,
|
||||||
"channels": channels,
|
"response": results[0],
|
||||||
"response": responses[0],
|
"responses": results,
|
||||||
"responses": responses,
|
|
||||||
"live": live,
|
"live": live,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/schedule/{channel}", response_class=RedirectResponse)
|
|
||||||
async def get_channel_default(channel: str):
|
@router.get("/schedule/{channel}", response_class=RedirectResponse)
|
||||||
|
async def get_channel_default(channel: str):
|
||||||
return RedirectResponse(f"{channel}/tag/today")
|
return RedirectResponse(f"{channel}/tag/today")
|
||||||
|
|
||||||
@app.get("/schedule/{channel}/tag/{tag}", response_class=HTMLResponse)
|
|
||||||
async def get_channel_tag(request: AppRequest, channel: str, tag: str):
|
@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)
|
tag_value = TagUtil.parse_tag(tag)
|
||||||
schedule_api = request.app.state.api.schedule
|
schedule_api = request.app.state.api.schedule
|
||||||
if tag_value.type == TagType.DAY:
|
if tag_value.type == TagType.DAY:
|
||||||
|
|||||||
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%;
|
|
||||||
}
|
|
||||||
@@ -2,27 +2,24 @@
|
|||||||
{% block title %}
|
{% block title %}
|
||||||
Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}
|
Программа | {{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/schedule/style.css?v={{version}}">
|
|
||||||
<link rel="icon"
|
|
||||||
href="/static/schedule/favicon.ico?v={{version}}"
|
|
||||||
type="image/x-icon">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
<span class="fs-4 text-body ms-2 me-2">/</span>
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
<app-link href="/schedule"
|
||||||
<a class="button"
|
icon="tv">{{_("TV program")}}</app-link>
|
||||||
href="../..">⬆️</a>
|
|
||||||
<span>{{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
|
||||||
<a class="button"
|
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<table>
|
<h4>
|
||||||
|
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
||||||
|
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
||||||
|
<a class="button"
|
||||||
|
href="../..">⬆️</a>
|
||||||
|
<span>{{response.channel.name}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
||||||
|
<a class="button"
|
||||||
|
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
||||||
|
</h4>
|
||||||
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -32,7 +29,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<tr class="{{'live' if value.live else ''}}">
|
<tr class="{{'table-success' if value.live else ''}}">
|
||||||
<td>{{value.start.strftime('%H:%M')}}</td>
|
<td>{{value.start.strftime('%H:%M')}}</td>
|
||||||
<td>{{(value.end - value.start) | timedelta_format}}</td>
|
<td>{{(value.end - value.start) | timedelta_format}}</td>
|
||||||
<td>{{value.label}}</td>
|
<td>{{value.label}}</td>
|
||||||
|
|||||||
@@ -1,21 +1,18 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}ТВ{% endblock %}
|
{% block title %}{{_("TV program")}}{% endblock %}
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/schedule/style.css?v={{version}}">
|
|
||||||
<link rel="icon"
|
|
||||||
href="/static/schedule/favicon.ico?v={{version}}"
|
|
||||||
type="image/x-icon">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header %}Телепрограмма{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul class="app-list">
|
<h1>{{_("TV program")}}</h1>
|
||||||
<li style="margin-bottom: 0.25rem; font-weight: bold;"><a href="schedule/tag/today">Все</a></li>
|
<div class="list-group mb-5">
|
||||||
|
<a href="schedule/tag/today"
|
||||||
|
class="list-group-item list-group-item-action px-4">
|
||||||
|
<span class="fw-bold">Все</span>
|
||||||
|
</a>
|
||||||
{% for channel in channels %}
|
{% for channel in channels %}
|
||||||
<li><a href="schedule/{{channel.id}}">{{channel.name}}</a></li>
|
<a href="schedule/{{channel.id}}"
|
||||||
|
class="list-group-item list-group-item-action px-4">
|
||||||
|
<span class="text-primary">{{channel.name}}</span>
|
||||||
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,29 +1,26 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{'Прямые трансляции' if live else 'Программа'}} | {{response.date.strftime('%a, %d %B %Y')}}
|
{{'Прямые трансляции' if live else _("TV program")}} | {{response.date.strftime('%a, %d %B %Y')}}
|
||||||
{% endblock %}
|
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/schedule/style.css?v={{version}}">
|
|
||||||
<link rel="icon"
|
|
||||||
href="/static/schedule/favicon.ico?v={{version}}"
|
|
||||||
type="image/x-icon">
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
<span class="fs-4 text-body ms-2 me-2">/</span>
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
<app-link href="/schedule"
|
||||||
<a class="button"
|
icon="tv">{{_("TV program")}}</app-link>
|
||||||
href="..">⬆️</a>
|
|
||||||
<span>{{'Прямые трансляции' if live else 'Программа'}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
|
||||||
<a class="button"
|
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<h4>
|
||||||
|
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
||||||
|
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
||||||
|
<a class="button"
|
||||||
|
href="..">⬆️</a>
|
||||||
|
<span>{{'Прямые трансляции' if live else 'Программа'}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
||||||
|
<a class="button"
|
||||||
|
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
||||||
|
</h4>
|
||||||
<div>
|
<div>
|
||||||
<table class="{{'live' if live else ''}}">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -35,15 +32,15 @@
|
|||||||
{% for response in responses %}
|
{% for response in responses %}
|
||||||
{% set values = (response.values|selectattr('live') if live else response.values)|list %}
|
{% set values = (response.values|selectattr('live') if live else response.values)|list %}
|
||||||
{% if values|length > 0 %}
|
{% if values|length > 0 %}
|
||||||
<tr>
|
<tr class="table-primary fs-4">
|
||||||
<td colspan="3">
|
<td colspan="3">
|
||||||
<div class="title">{{response.channel.name}}</div>
|
<div>{{response.channel.name}}</div>
|
||||||
</td>
|
</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% for value in values %}
|
{% for value in values %}
|
||||||
<tr class="{{'live' if not live and value.live else ''}}">
|
<tr class="{{'table-success' if not live and value.live else ''}}">
|
||||||
<td>{{value.start.strftime('%H:%M')}}</td>
|
<td>{{value.start.strftime('%H:%M')}}</td>
|
||||||
<td>{{(value.end - value.start) | timedelta_format}}</td>
|
<td>{{(value.end - value.start) | timedelta_format}}</td>
|
||||||
<td>{{value.label}}</td>
|
<td>{{value.label}}</td>
|
||||||
|
|||||||
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,34 +1,33 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI
|
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.easel.core import AppRequest
|
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"))
|
|
||||||
templates = Jinja2Templates(
|
|
||||||
directory=[
|
directory=[
|
||||||
base_dir.parent / "common/templates",
|
base_dir.parent / "common/templates",
|
||||||
base_dir / "templates",
|
base_dir / "templates",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
templates.env.globals.update({"_": _})
|
||||||
templates.env.filters["cloudness_icon"] = cloudness_icon
|
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
||||||
|
templates.env.filters["cloudness_icon"] = cloudness_icon
|
||||||
|
|
||||||
def build_weather_response(request: AppRequest, response: WeatherResponse):
|
|
||||||
|
def build_weather_response(request: AppRequest, response: WeatherResponse):
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="weather.html",
|
name="weather.html",
|
||||||
@@ -40,48 +39,43 @@ def mount(app: FastAPI):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/weather", response_class=HTMLResponse)
|
router = APIRouter()
|
||||||
async def get_weather_list(request: AppRequest):
|
|
||||||
|
@router.get("/weather", response_class=HTMLResponse)
|
||||||
|
async def get_weather_index(request: AppRequest, query: str | None = None):
|
||||||
weather_api = request.app.state.api.weather
|
weather_api = request.app.state.api.weather
|
||||||
locations = await weather_api.get_locations()
|
locations = (await weather_api.find_locations(query)) if query else []
|
||||||
locations_data = BUNDLE.select_items(locations)
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request=request,
|
request=request,
|
||||||
name="index.html",
|
name="index.html",
|
||||||
context={
|
context={
|
||||||
"version": __version__,
|
"version": __version__,
|
||||||
"locations": locations_data,
|
"locations": locations,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.get("/weather/{location}", response_class=RedirectResponse)
|
|
||||||
async def get_weather_default(location: str):
|
@router.get("/weather/{location}", response_class=RedirectResponse)
|
||||||
|
async def get_weather_default(location: str):
|
||||||
return RedirectResponse(f"{location}/tag/today")
|
return RedirectResponse(f"{location}/tag/today")
|
||||||
|
|
||||||
@app.get("/weather/{location}/day/mock", response_class=HTMLResponse)
|
|
||||||
async def get_weather_day_mock(request: AppRequest):
|
|
||||||
response = WEATHER_MOCK_DATA.get_response("day")
|
|
||||||
return build_weather_response(request, response)
|
|
||||||
|
|
||||||
@app.get("/weather/{location}/days/mock", response_class=HTMLResponse)
|
@router.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
|
||||||
async def get_weather_days_mock(request: AppRequest):
|
async def get_weather_day(request: AppRequest, location: str, date: datetime.date):
|
||||||
response = WEATHER_MOCK_DATA.get_response("days")
|
|
||||||
return build_weather_response(request, response)
|
|
||||||
|
|
||||||
@app.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
|
|
||||||
async def get_weather_day(request: AppRequest, location: str, date: datetime.date):
|
|
||||||
weather_api = request.app.state.api.weather
|
weather_api = request.app.state.api.weather
|
||||||
response = await weather_api.get_day(location, date)
|
response = await weather_api.get_day(location, date)
|
||||||
return build_weather_response(request, response)
|
return build_weather_response(request, response)
|
||||||
|
|
||||||
@app.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
|
|
||||||
async def get_weather_days(request: AppRequest, location: str, days: int):
|
@router.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
|
||||||
|
async def get_weather_days(request: AppRequest, location: str, days: int):
|
||||||
weather_api = request.app.state.api.weather
|
weather_api = request.app.state.api.weather
|
||||||
response = await weather_api.get_days(location, days)
|
response = await weather_api.get_days(location, days)
|
||||||
return build_weather_response(request, response)
|
return build_weather_response(request, response)
|
||||||
|
|
||||||
@app.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse)
|
|
||||||
async def get_weather_tag(request: AppRequest, location: str, tag: str):
|
@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)
|
tag_value = TagUtil.parse_tag(tag)
|
||||||
weather_api = request.app.state.api.weather
|
weather_api = request.app.state.api.weather
|
||||||
if tag_value.type == TagType.DAY:
|
if tag_value.type == TagType.DAY:
|
||||||
|
|||||||
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,20 +1,73 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Погода{% endblock %}
|
{% block title %}Weather{% endblock %}
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/weather/style.css?v={{version}}">
|
|
||||||
<link rel="icon"
|
|
||||||
href="/static/weather/favicon.ico?v={{version}}"
|
|
||||||
type="image/x-icon">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header %}Погода{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<ul class="app-list">
|
<h1>Weather</h1>
|
||||||
|
<form action=""
|
||||||
|
method="get"
|
||||||
|
class="mb-4">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="query"
|
||||||
|
name="query"
|
||||||
|
placeholder="Enter the city name">
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
type="submit">Search</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<ul id="locations"
|
||||||
|
class="list-group mb-5">
|
||||||
{% for location in locations %}
|
{% for location in locations %}
|
||||||
<li><a href="weather/{{location.id}}">{{location.name}}</a></li>
|
<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 %}
|
{% endfor %}
|
||||||
</ul>
|
</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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.saveLocation = (location) => {
|
||||||
|
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
|
||||||
|
locations[location.id] = location.name;
|
||||||
|
window.localStorage.setItem('locations', JSON.stringify(locations));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 %}
|
{% endblock %}
|
||||||
@@ -1,32 +1,29 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Погода | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}{% endblock %}
|
{% block title %}{{_("Weather")}} | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}{% endblock %}
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="/static/weather/style.css?v={{version}}">
|
|
||||||
<link rel="icon"
|
|
||||||
href="/static/weather/favicon.ico?v={{version}}"
|
|
||||||
type="image/x-icon">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
{% if response.period == 'day' %}
|
<span class="fs-4 text-body ms-2 me-2">/</span>
|
||||||
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
<app-link href="/weather" icon="brightness-high">{{_("Weather")}}</app-link>
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
|
||||||
<a class="button"
|
|
||||||
href="../tag/days-10">⬆️</a>
|
|
||||||
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
|
||||||
<a class="button"
|
|
||||||
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
|
||||||
{% endif %}
|
|
||||||
{% if response.period == 'days' %}
|
|
||||||
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div>
|
<h4>
|
||||||
<table style="margin: auto;">
|
{% if response.period == 'day' %}
|
||||||
|
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
||||||
|
href="../tag/{{tag_util.create_tag('day', response.date, -1)}}">⬅️</a>
|
||||||
|
<a class="button"
|
||||||
|
href="../tag/days-10">⬆️</a>
|
||||||
|
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
||||||
|
<a class="button"
|
||||||
|
href="../tag/{{tag_util.create_tag('day', response.date, 1)}}">➡️</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if response.period == 'days' %}
|
||||||
|
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-weather table-borderless table-compact text-center w-auto"
|
||||||
|
style="font-size: 130%;">
|
||||||
<tbody>
|
<tbody>
|
||||||
<!-- date -->
|
<!-- date -->
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ 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.openweather.api import OpenWeatherApi
|
||||||
|
from gallery.painting.yandextv.api import YandexTvApi
|
||||||
from gallery.sketch.bundle import ApiBundle
|
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
|
||||||
|
|
||||||
api = ApiBundle(
|
api = ApiBundle(
|
||||||
[
|
[
|
||||||
|
CachedScheduleApi(YandexTvApi()),
|
||||||
CachedScheduleApi(MatchTvApi()),
|
CachedScheduleApi(MatchTvApi()),
|
||||||
CachedWeatherApi(GismeteoApi()),
|
CachedWeatherApi(GismeteoApi()),
|
||||||
CachedWeatherApi(OpenWeatherApi()),
|
CachedWeatherApi(OpenWeatherApi()),
|
||||||
@@ -24,8 +26,8 @@ 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,29 @@ 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"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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)}")
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from gallery.sketch.mock import MockData
|
|
||||||
|
|
||||||
GISMETEO_MOCK_DATA = MockData(Path(__file__).parent / "data")
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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,7 +26,7 @@ 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"tvguide/{channel_id}?date={date:%Y%m%d}"
|
endpoint = f"tvguide/{channel_id}?date={date:%Y%m%d}"
|
||||||
data = await self.SOURCE.request(endpoint)
|
data = await self.SOURCE.request(endpoint)
|
||||||
@@ -46,8 +45,12 @@ class MatchTvApi(ScheduleApi):
|
|||||||
for item in soup.select(
|
for item in soup.select(
|
||||||
".p-tv-guide-schedule-channel-carcass__transmissions .p-tv-guide-schedule-channel-transmission"
|
".p-tv-guide-schedule-channel-carcass__transmissions .p-tv-guide-schedule-channel-transmission"
|
||||||
):
|
):
|
||||||
title = item.select_one(".p-tv-guide-schedule-channel-transmission__title").text.strip()
|
title = item.select_one(
|
||||||
time_str = item.select_one(".p-tv-guide-schedule-channel-transmission__time-block").text.strip()
|
".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
@@ -5,8 +5,7 @@ from collections import defaultdict
|
|||||||
from aiocache import cached
|
from aiocache import cached
|
||||||
|
|
||||||
from gallery.sketch.weather.api import WeatherApi
|
from gallery.sketch.weather.api import WeatherApi
|
||||||
from gallery.sketch.weather.catalog import BUNDLE, LocationId
|
from gallery.sketch.weather.model import Location, WeatherResponse, WeatherValue
|
||||||
from gallery.sketch.weather.model import WeatherResponse, WeatherValue
|
|
||||||
from gallery.sketch.weather.util import merge_weather_values
|
from gallery.sketch.weather.util import merge_weather_values
|
||||||
from gallery.util import TimeUnit
|
from gallery.util import TimeUnit
|
||||||
|
|
||||||
@@ -20,11 +19,9 @@ class OpenWeatherApi(WeatherApi):
|
|||||||
PROVIDER = "openweather"
|
PROVIDER = "openweather"
|
||||||
SOURCE = OpenWeather("517a6bccceaa1c48127f6199ec3fb7cf")
|
SOURCE = OpenWeather("517a6bccceaa1c48127f6199ec3fb7cf")
|
||||||
|
|
||||||
async def get_locations(self) -> list[str]:
|
@classmethod
|
||||||
return [
|
def _parse_location(cls, location_id: str) -> tuple[float, float]:
|
||||||
LocationId.OREL,
|
return tuple(map(float, location_id.split(":", maxsplit=2)))
|
||||||
LocationId.ZMIYEVKA,
|
|
||||||
]
|
|
||||||
|
|
||||||
@cached(
|
@cached(
|
||||||
key_builder=lambda fun, self, location_id: f"api.weather.{self.provider}.source.{location_id}.forecast",
|
key_builder=lambda fun, self, location_id: f"api.weather.{self.provider}.source.{location_id}.forecast",
|
||||||
@@ -32,8 +29,10 @@ class OpenWeatherApi(WeatherApi):
|
|||||||
ttl=TimeUnit.DAY,
|
ttl=TimeUnit.DAY,
|
||||||
)
|
)
|
||||||
async def _get_location_forecast(self, location_id: str) -> Forecast:
|
async def _get_location_forecast(self, location_id: str) -> Forecast:
|
||||||
location = BUNDLE.get_item(location_id)
|
return await self.SOURCE.get_forecast(*self._parse_location(location_id))
|
||||||
return await self.SOURCE.get_forecast(location.lat, location.lon)
|
|
||||||
|
async def find_locations(self, query: str) -> list[Location]:
|
||||||
|
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:
|
||||||
data: Forecast = await self._get_location_forecast(location_id)
|
data: Forecast = await self._get_location_forecast(location_id)
|
||||||
@@ -42,9 +41,8 @@ class OpenWeatherApi(WeatherApi):
|
|||||||
value = FORECAST_ITEM_PARSER.parse(item)
|
value = FORECAST_ITEM_PARSER.parse(item)
|
||||||
if value.date.date() == date:
|
if value.date.date() == date:
|
||||||
values.append(value)
|
values.append(value)
|
||||||
location = BUNDLE.get_item(location_id)
|
|
||||||
return WeatherResponse(
|
return WeatherResponse(
|
||||||
location=location.name,
|
location=location_id,
|
||||||
date=date,
|
date=date,
|
||||||
period="day",
|
period="day",
|
||||||
values=values,
|
values=values,
|
||||||
@@ -61,9 +59,8 @@ class OpenWeatherApi(WeatherApi):
|
|||||||
merge_weather_values(date, values)
|
merge_weather_values(date, values)
|
||||||
for date, values in values_by_date.items()
|
for date, values in values_by_date.items()
|
||||||
]
|
]
|
||||||
location = BUNDLE.get_item(location_id)
|
|
||||||
return WeatherResponse(
|
return WeatherResponse(
|
||||||
location=location.name,
|
location=location_id,
|
||||||
date=datetime.date.today(),
|
date=datetime.date.today(),
|
||||||
period="days",
|
period="days",
|
||||||
values=list(sorted(values, key=lambda item: item.date)),
|
values=list(sorted(values, key=lambda item: item.date)),
|
||||||
|
|||||||
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,5 +1,3 @@
|
|||||||
from typing import Type
|
|
||||||
|
|
||||||
from .api import API, Api
|
from .api import API, Api
|
||||||
from .schedule.api import ScheduleApi
|
from .schedule.api import ScheduleApi
|
||||||
from .weather.api import WeatherApi
|
from .weather.api import WeatherApi
|
||||||
@@ -15,7 +13,7 @@ class ApiBundle(list[Api]):
|
|||||||
return value
|
return value
|
||||||
raise ValueError(provider)
|
raise ValueError(provider)
|
||||||
|
|
||||||
def get_api_by_type(self, api_type: Type[API]) -> API:
|
def get_api_by_type(self, api_type: type[API]) -> API:
|
||||||
for value in self:
|
for value in self:
|
||||||
if isinstance(value, api_type):
|
if isinstance(value, api_type):
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
from typing import Generic
|
from typing import Generic, NamedTuple
|
||||||
|
|
||||||
from gallery.util import TimeUnit
|
from gallery.util import TimeUnit
|
||||||
|
|
||||||
from .api import API, Api
|
from .api import API, 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):
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class MockData:
|
|
||||||
|
|
||||||
def __init__(self, data_dir) -> None:
|
|
||||||
self._data_dir = data_dir
|
|
||||||
|
|
||||||
def get_text(self, key: str) -> str:
|
|
||||||
return (self._data_dir / f"{key}").read_text()
|
|
||||||
|
|
||||||
def get_html(self, key: str) -> str:
|
|
||||||
return self.get_text(f"{key}.html")
|
|
||||||
|
|
||||||
def get_json(self, key: str) -> dict:
|
|
||||||
data = json.loads(self.get_text(f"{key}.json"))
|
|
||||||
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,31 +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="Орёл",
|
|
||||||
lat=52.9687747,
|
|
||||||
lon=36.0694937,
|
|
||||||
),
|
|
||||||
Location(
|
|
||||||
id=LocationId.ZMIYEVKA,
|
|
||||||
name="Змиёвка",
|
|
||||||
lat=52.672192,
|
|
||||||
lon=36.380112,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -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}]}
|
|
||||||
@@ -14,6 +14,9 @@ class Location(Model):
|
|||||||
name: str
|
name: str
|
||||||
lat: float
|
lat: float
|
||||||
lon: float
|
lon: float
|
||||||
|
country: str
|
||||||
|
district: str
|
||||||
|
subdistrict: str
|
||||||
|
|
||||||
|
|
||||||
class Cloudness(str, Enum):
|
class Cloudness(str, Enum):
|
||||||
|
|||||||
@@ -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.0"
|
||||||
|
|||||||
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.0"
|
||||||
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.0",
|
||||||
|
"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
10
tests/data/openweather/__init__.py
Normal file
10
tests/data/openweather/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tests.common.mock import MockSource
|
||||||
|
|
||||||
|
OPENWEATHER_MOCK_SOURCE = MockSource(
|
||||||
|
Path(__file__).parent,
|
||||||
|
{
|
||||||
|
"forecast": "forecast.json",
|
||||||
|
},
|
||||||
|
)
|
||||||
10
tests/data/yandextv/__init__.py
Normal file
10
tests/data/yandextv/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from tests.common.mock import MockSource
|
||||||
|
|
||||||
|
YANDEXTV_MOCK_SOURCE = MockSource(
|
||||||
|
Path(__file__).parent,
|
||||||
|
{
|
||||||
|
"test": "test.html",
|
||||||
|
},
|
||||||
|
)
|
||||||
88
tests/data/yandextv/test.html
Normal file
88
tests/data/yandextv/test.html
Normal file
File diff suppressed because one or more lines are too long
@@ -3,20 +3,21 @@ import datetime
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from gallery.painting.gismeteo.api import GismeteoApi
|
from gallery.painting.gismeteo.api import GismeteoApi
|
||||||
from gallery.painting.gismeteo.mock import GISMETEO_MOCK_DATA
|
from tests.data.gismeteo import GISMETEO_MOCK_SOURCE
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="gismeteo_api", scope="module")
|
@pytest.fixture(name="gismeteo_api", scope="module")
|
||||||
def gismeteo_api_fixture() -> GismeteoApi:
|
def gismeteo_api_fixture() -> GismeteoApi:
|
||||||
class MockSource:
|
|
||||||
async def request(self, endpoint: str):
|
|
||||||
return GISMETEO_MOCK_DATA.get_html(endpoint.split("/")[-1])
|
|
||||||
|
|
||||||
api = GismeteoApi()
|
api = GismeteoApi()
|
||||||
api.SOURCE = MockSource()
|
api.SOURCE = GISMETEO_MOCK_SOURCE
|
||||||
return api
|
return api
|
||||||
|
|
||||||
|
|
||||||
|
async def test_search(gismeteo_api: GismeteoApi):
|
||||||
|
result = await gismeteo_api.find_locations("test")
|
||||||
|
assert len(result) == 10
|
||||||
|
|
||||||
|
|
||||||
async def test_day(gismeteo_api: GismeteoApi):
|
async def test_day(gismeteo_api: GismeteoApi):
|
||||||
result = await gismeteo_api.get_day("test", datetime.date.today())
|
result = await gismeteo_api.get_day("test", datetime.date.today())
|
||||||
assert len(result.values) == 8
|
assert len(result.values) == 8
|
||||||
|
|||||||
@@ -3,21 +3,20 @@ import datetime
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from gallery.painting.matchtv.api import MatchTvApi
|
from gallery.painting.matchtv.api import MatchTvApi
|
||||||
from gallery.painting.matchtv.mock import MATCHTV_MOCK_DATA
|
from gallery.sketch.schedule.model import ChannelId
|
||||||
|
from tests.data.matchtv import MATCHTV_MOCK_SOURCE
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="matchtv_api", scope="module")
|
@pytest.fixture(name="matchtv_api", scope="module")
|
||||||
def matchtv_api_fixture() -> MatchTvApi:
|
def matchtv_api_fixture() -> MatchTvApi:
|
||||||
class MockSource:
|
|
||||||
async def request(self, endpoint: str):
|
|
||||||
return MATCHTV_MOCK_DATA.get_html(endpoint.split("/")[1])
|
|
||||||
|
|
||||||
api = MatchTvApi()
|
api = MatchTvApi()
|
||||||
api.SOURCE = MockSource()
|
api.SOURCE = MATCHTV_MOCK_SOURCE
|
||||||
return api
|
return api
|
||||||
|
|
||||||
|
|
||||||
async def test_channel(matchtv_api: MatchTvApi):
|
async def test_channel(matchtv_api: MatchTvApi):
|
||||||
result = await matchtv_api.get_channel_schedule("test", datetime.date.today())
|
result = await matchtv_api.get_channel_schedule(
|
||||||
|
ChannelId.TEST, datetime.date.today()
|
||||||
|
)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert len(result.values) > 0
|
assert len(result.values) > 0
|
||||||
|
|||||||
@@ -3,25 +3,27 @@ import datetime
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from gallery.painting.openweather.api import OpenWeatherApi
|
from gallery.painting.openweather.api import OpenWeatherApi
|
||||||
from gallery.painting.openweather.mock import OPENWEATHER_MOCK_DATA
|
from gallery.painting.openweather.openweather import OpenWeather
|
||||||
from gallery.painting.openweather.openweather import Forecast
|
from tests.data.openweather import OPENWEATHER_MOCK_SOURCE
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="openweather_api", scope="module")
|
@pytest.fixture(name="openweather_api", scope="module")
|
||||||
def openweather_api_fixture() -> OpenWeatherApi:
|
def openweather_api_fixture() -> OpenWeatherApi:
|
||||||
async def _get_location_forecast(location_id: str) -> Forecast:
|
class MockOpenWeather(OpenWeather):
|
||||||
return Forecast(**OPENWEATHER_MOCK_DATA.get_json("forecast"))
|
def __init__(self):
|
||||||
|
super().__init__("")
|
||||||
|
self._source = OPENWEATHER_MOCK_SOURCE
|
||||||
|
|
||||||
api = OpenWeatherApi()
|
api = OpenWeatherApi()
|
||||||
api._get_location_forecast = _get_location_forecast
|
api.SOURCE = MockOpenWeather()
|
||||||
return api
|
return api
|
||||||
|
|
||||||
|
|
||||||
async def test_day(openweather_api: OpenWeatherApi):
|
async def test_day(openweather_api: OpenWeatherApi):
|
||||||
result = await openweather_api.get_day("orel-4432", datetime.date(2024, 8, 23))
|
result = await openweather_api.get_day("52.968498:36.0695", datetime.date(2024, 8, 23))
|
||||||
assert len(result.values) == 8
|
assert len(result.values) == 8
|
||||||
|
|
||||||
|
|
||||||
async def test_days(openweather_api: OpenWeatherApi):
|
async def test_days(openweather_api: OpenWeatherApi):
|
||||||
result = await openweather_api.get_days("orel-4432", 10)
|
result = await openweather_api.get_days("52.968498:36.0695", 10)
|
||||||
assert len(result.values) == 6
|
assert len(result.values) == 6
|
||||||
|
|||||||
23
tests/test_yandextv_api.py
Normal file
23
tests/test_yandextv_api.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from gallery.painting.yandextv.api import CHANNELS_MAP, YandexTvApi
|
||||||
|
from gallery.sketch.schedule.model import ChannelId
|
||||||
|
from tests.data.yandextv import YANDEXTV_MOCK_SOURCE
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="yandextv_api", scope="module")
|
||||||
|
def yandextv_api_fixture() -> YandexTvApi:
|
||||||
|
api = YandexTvApi()
|
||||||
|
api.SOURCE = YANDEXTV_MOCK_SOURCE
|
||||||
|
CHANNELS_MAP[ChannelId("test")] = "test"
|
||||||
|
return api
|
||||||
|
|
||||||
|
|
||||||
|
async def test_channel(yandextv_api: YandexTvApi):
|
||||||
|
result = await yandextv_api.get_channel_schedule(
|
||||||
|
ChannelId.TEST, datetime.date.today()
|
||||||
|
)
|
||||||
|
assert result is not None
|
||||||
|
assert len(result.values) > 0
|
||||||
Reference in New Issue
Block a user