refactor: rename project to gallery
This commit is contained in:
0
gallery/__init__.py
Normal file
0
gallery/__init__.py
Normal file
21
gallery/easel/__init__.py
Normal file
21
gallery/easel/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import locale
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from gallery.sketch.weather.api import WeatherApi
|
||||
|
||||
from .route import api, doc, view
|
||||
|
||||
|
||||
def build_app(weather_api: WeatherApi) -> FastAPI:
|
||||
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
|
||||
app = FastAPI(
|
||||
title="Weather",
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
)
|
||||
app.state.weather_api = weather_api
|
||||
doc.mount(app)
|
||||
api.mount(app)
|
||||
view.mount(app)
|
||||
return app
|
||||
0
gallery/easel/route/__init__.py
Normal file
0
gallery/easel/route/__init__.py
Normal file
22
gallery/easel/route/api.py
Normal file
22
gallery/easel/route/api.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import datetime
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
|
||||
from gallery.sketch.weather.api import WeatherApi
|
||||
from gallery.sketch.weather.model import WeatherResponse
|
||||
|
||||
|
||||
def mount(app: FastAPI):
|
||||
@app.get("/api/weather/{location}/day/{date}")
|
||||
async def get_api_weather_day(
|
||||
request: Request, location: str, date: datetime.date
|
||||
) -> WeatherResponse:
|
||||
weather_api: WeatherApi = request.app.state.weather_api
|
||||
return await weather_api.get_day(location, date)
|
||||
|
||||
@app.get("/api/weather/{location}/days/{days}")
|
||||
async def get_api_weather_days(
|
||||
request: Request, location: str, days: int
|
||||
) -> WeatherResponse:
|
||||
weather_api: WeatherApi = request.app.state.weather_api
|
||||
return await weather_api.get_days(location, days)
|
||||
37
gallery/easel/route/doc/__init__.py
Normal file
37
gallery/easel/route/doc/__init__.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from anyio import Path
|
||||
from fastapi import FastAPI
|
||||
from fastapi.openapi.docs import (
|
||||
get_redoc_html,
|
||||
get_swagger_ui_html,
|
||||
get_swagger_ui_oauth2_redirect_html,
|
||||
)
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
|
||||
def mount(app: FastAPI):
|
||||
app.mount(
|
||||
"/docs/static",
|
||||
StaticFiles(directory=Path(__file__).parent / "static"),
|
||||
)
|
||||
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
async def custom_swagger_ui_html():
|
||||
return get_swagger_ui_html(
|
||||
openapi_url=app.openapi_url,
|
||||
title=app.title + " - Swagger UI",
|
||||
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
|
||||
swagger_js_url="docs/static/swagger-ui-bundle.js",
|
||||
swagger_css_url="docs/static/swagger-ui.css",
|
||||
)
|
||||
|
||||
@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False)
|
||||
async def swagger_ui_redirect():
|
||||
return get_swagger_ui_oauth2_redirect_html()
|
||||
|
||||
@app.get("/redoc", include_in_schema=False)
|
||||
async def redoc_html():
|
||||
return get_redoc_html(
|
||||
openapi_url=app.openapi_url,
|
||||
title=app.title + " - ReDoc",
|
||||
redoc_js_url="docs/static/redoc.standalone.js",
|
||||
)
|
||||
1782
gallery/easel/route/doc/static/redoc.standalone.js
vendored
Normal file
1782
gallery/easel/route/doc/static/redoc.standalone.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
gallery/easel/route/doc/static/swagger-ui-bundle.js
vendored
Normal file
2
gallery/easel/route/doc/static/swagger-ui-bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
gallery/easel/route/doc/static/swagger-ui.css
vendored
Normal file
3
gallery/easel/route/doc/static/swagger-ui.css
vendored
Normal file
File diff suppressed because one or more lines are too long
57
gallery/easel/route/util.py
Normal file
57
gallery/easel/route/util.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class TagType(str, Enum):
|
||||
DAY = "day"
|
||||
DAYS = "days"
|
||||
|
||||
|
||||
class Tag(NamedTuple):
|
||||
type: TagType
|
||||
date: datetime.date
|
||||
days: int = 1
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.type == TagType.DAY:
|
||||
today = datetime.date.today()
|
||||
day = (self.date - today).days
|
||||
return f"day-{day}"
|
||||
elif self.type == TagType.DAYS:
|
||||
return f"days-{self.days}"
|
||||
else:
|
||||
raise ValueError(self.type)
|
||||
|
||||
|
||||
class TagUtil:
|
||||
@classmethod
|
||||
def parse_tag(cls, tag: str) -> Tag:
|
||||
if tag == "today":
|
||||
return Tag(TagType.DAY, datetime.date.today())
|
||||
elif tag == "tomorrow":
|
||||
return Tag(TagType.DAY, datetime.date.today() + datetime.timedelta(days=1))
|
||||
elif tag.startswith("day-"):
|
||||
days = int(tag.split("-")[-1])
|
||||
return Tag(
|
||||
TagType.DAY,
|
||||
datetime.date.today() + datetime.timedelta(days=days),
|
||||
)
|
||||
elif tag.startswith("days-"):
|
||||
days = int(tag.split("-")[-1])
|
||||
return Tag(TagType.DAYS, datetime.date.today(), days)
|
||||
raise ValueError(tag)
|
||||
|
||||
@classmethod
|
||||
def create_tag(
|
||||
cls,
|
||||
tag_type: TagType,
|
||||
date: datetime.date,
|
||||
day: int = 0,
|
||||
days: int = 10,
|
||||
) -> Tag:
|
||||
if tag_type == TagType.DAY:
|
||||
return Tag(tag_type, date + datetime.timedelta(days=day))
|
||||
if tag_type == TagType.DAYS:
|
||||
return Tag(tag_type, date, days)
|
||||
raise ValueError(tag_type)
|
||||
87
gallery/easel/route/view/__init__.py
Normal file
87
gallery/easel/route/view/__init__.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from gallery.sketch.weather.api import WeatherApi
|
||||
from gallery.sketch.weather.mock import WEATHER_MOCK_DATA
|
||||
from gallery.sketch.weather.model import WeatherResponse
|
||||
|
||||
from ..util import TagType, TagUtil
|
||||
from .filters import cloudness_icon, wind_direction_icon
|
||||
|
||||
|
||||
def mount(app: FastAPI):
|
||||
base_dir = Path(__file__).parent
|
||||
app.mount("/static", StaticFiles(directory=base_dir / "static"), name="static")
|
||||
templates = Jinja2Templates(directory=base_dir / "templates")
|
||||
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
||||
templates.env.filters["cloudness_icon"] = cloudness_icon
|
||||
|
||||
def build_weather_response(request: Request, response: WeatherResponse):
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="weather.html",
|
||||
context={
|
||||
"tag_util": TagUtil,
|
||||
"datetime": datetime,
|
||||
"response": response,
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/", response_class=RedirectResponse)
|
||||
async def get_weather_root():
|
||||
return RedirectResponse("weather")
|
||||
|
||||
@app.get("/weather", response_class=HTMLResponse)
|
||||
async def get_weather_list(request: Request):
|
||||
weather_api: WeatherApi = request.app.state.weather_api
|
||||
locations = await weather_api.get_locations()
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="index.html",
|
||||
context={
|
||||
"locations": locations,
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/weather/{location}", response_class=RedirectResponse)
|
||||
async def get_weather_default(location: str):
|
||||
return RedirectResponse(f"{location}/tag/today")
|
||||
|
||||
@app.get("/weather/{location}/day/mock", response_class=HTMLResponse)
|
||||
async def get_weather_day_mock(request: Request):
|
||||
response = WEATHER_MOCK_DATA.get_response("day")
|
||||
return build_weather_response(request, response)
|
||||
|
||||
@app.get("/weather/{location}/days/mock", response_class=HTMLResponse)
|
||||
async def get_weather_days_mock(request: Request):
|
||||
response = WEATHER_MOCK_DATA.get_response("days")
|
||||
return build_weather_response(request, response)
|
||||
|
||||
@app.get("/weather/{location}/day/{date}", response_class=HTMLResponse)
|
||||
async def get_weather_day(request: Request, location: str, date: datetime.date):
|
||||
weather_api: WeatherApi = request.app.state.weather_api
|
||||
response = await weather_api.get_day(location, date)
|
||||
return build_weather_response(request, response)
|
||||
|
||||
@app.get("/weather/{location}/days/{days}", response_class=HTMLResponse)
|
||||
async def get_weather_days(request: Request, location: str, days: int):
|
||||
weather_api: WeatherApi = request.app.state.weather_api
|
||||
response = await weather_api.get_days(location, days)
|
||||
return build_weather_response(request, response)
|
||||
|
||||
@app.get("/weather/{location}/tag/{tag}", response_class=HTMLResponse)
|
||||
async def get_weather_tag(request: Request, location: str, tag: str):
|
||||
tag_value = TagUtil.parse_tag(tag)
|
||||
weather_api: WeatherApi = request.app.state.weather_api
|
||||
if tag_value.type == TagType.DAY:
|
||||
response = await weather_api.get_day(location, tag_value.date)
|
||||
elif tag_value.type == TagType.DAYS:
|
||||
response = await weather_api.get_days(location, tag_value.days)
|
||||
else:
|
||||
raise ValueError(tag)
|
||||
return build_weather_response(request, response)
|
||||
39
gallery/easel/route/view/filters.py
Normal file
39
gallery/easel/route/view/filters.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from gallery.sketch.weather.model import Cloudness, Precipitation, Sky, WindDirection
|
||||
|
||||
|
||||
def wind_direction_icon(wind_direction: WindDirection) -> str:
|
||||
return {
|
||||
WindDirection.N: "⬇️",
|
||||
WindDirection.NO: "↙️",
|
||||
WindDirection.O: "⬅️",
|
||||
WindDirection.SO: "↖️",
|
||||
WindDirection.S: "⬆️",
|
||||
WindDirection.SW: "↗️",
|
||||
WindDirection.W: "➡️",
|
||||
WindDirection.NW: "↘️",
|
||||
WindDirection.CALM: "",
|
||||
}.get(wind_direction, wind_direction)
|
||||
|
||||
|
||||
def cloudness_icon(sky: Sky) -> list[str]:
|
||||
main_icon = ""
|
||||
if sky.thunder:
|
||||
if sky.cloudness == Cloudness.CLEAR:
|
||||
main_icon = "🌩️"
|
||||
if sky.precipitation == Precipitation.NO:
|
||||
main_icon = "⚡"
|
||||
else:
|
||||
main_icon = "⛈️"
|
||||
elif sky.precipitation == Precipitation.NO:
|
||||
main_icon = {
|
||||
Cloudness.CLEAR: "☀️",
|
||||
Cloudness.PARTLY_CLOUDY: "🌤️",
|
||||
Cloudness.CLOUDY: "⛅",
|
||||
Cloudness.MAINLY_CLOUDY: "☁️",
|
||||
}[sky.cloudness]
|
||||
else:
|
||||
main_icon = "🌧️"
|
||||
icons = [main_icon]
|
||||
if sky.fog:
|
||||
icons.append("🌫️")
|
||||
return icons
|
||||
BIN
gallery/easel/route/view/static/favicon.ico
Normal file
BIN
gallery/easel/route/view/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
0
gallery/easel/route/view/static/index.js
Normal file
0
gallery/easel/route/view/static/index.js
Normal file
116
gallery/easel/route/view/static/style.css
Normal file
116
gallery/easel/route/view/static/style.css
Normal file
@@ -0,0 +1,116 @@
|
||||
body {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
a.button {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.button.disabled {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
color: gray;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
table {
|
||||
/* width: 100%; */
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
/* border: 1px solid rgba(0, 0, 0, 0.2); */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.1rem 0.4rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
26
gallery/easel/route/view/templates/index.html
Normal file
26
gallery/easel/route/view/templates/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible"
|
||||
content="ie=edge">
|
||||
<title>Погода</title>
|
||||
<link rel="stylesheet"
|
||||
href="/static/style.css?v=1">
|
||||
<link rel="icon"
|
||||
href="/static/favicon.ico"
|
||||
type="image/x-icon">
|
||||
</head>
|
||||
|
||||
<body class="app-container">
|
||||
<ul>
|
||||
{% for location in locations %}
|
||||
<li><a href="weather/{{location}}">{{location}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
177
gallery/easel/route/view/templates/weather.html
Normal file
177
gallery/easel/route/view/templates/weather.html
Normal file
@@ -0,0 +1,177 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible"
|
||||
content="ie=edge">
|
||||
<title>Погода | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</title>
|
||||
<link rel="stylesheet"
|
||||
href="/static/style.css?v=1">
|
||||
<link rel="icon"
|
||||
href="/static/favicon.ico"
|
||||
type="image/x-icon">
|
||||
</head>
|
||||
|
||||
<body class="app-container">
|
||||
<h3>
|
||||
{% 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 %}
|
||||
</h3>
|
||||
<table>
|
||||
<tbody>
|
||||
<!-- date -->
|
||||
<tr>
|
||||
{% for value in response.values %}
|
||||
{% if response.period == 'day' %}
|
||||
<td
|
||||
class="date {{'now' if value.date < datetime.datetime.now() and value.date + datetime.timedelta(hours=3) > datetime.datetime.now() else ''}}">
|
||||
<span class="value">{{value.date.strftime('%H:%M')}}</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if response.period == 'days' %}
|
||||
<td class="date {{'now' if value.date.date() == datetime.date.today() else ''}}">
|
||||
<span class="value">
|
||||
<a href="../tag/{{tag_util.create_tag('day', value.date.date())}}">
|
||||
{{value.date.strftime('%a %d')}}
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<!-- cloudness -->
|
||||
<tr>
|
||||
<td colspan="{{response.values | length}}"
|
||||
class="header">
|
||||
Облачность
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for value in response.values %}
|
||||
<td class="cloudness">
|
||||
{% for icon in value.sky | cloudness_icon %}
|
||||
<div class="icon">{{icon}}</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<!-- temperature -->
|
||||
<tr>
|
||||
<td colspan="{{response.values | length}}"
|
||||
class="header">
|
||||
Температура, °C
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for value in response.values %}
|
||||
<td class="temperature">
|
||||
{% for temperature in value.temperature %}
|
||||
<div class="value {{'positive' if temperature > 0 else 'negative'}}"
|
||||
style="background-color: rgba(255, 128, 128, {{(temperature - 10) * 0.015}});">
|
||||
{{temperature}}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<!-- wind_direction -->
|
||||
<tr>
|
||||
<td colspan="{{response.values | length}}"
|
||||
class="header">
|
||||
Направление ветра
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for value in response.values %}
|
||||
<td class="wind">
|
||||
<span class="icon">{{value.wind_direction | wind_direction_icon}}</span>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<!-- wind_speed -->
|
||||
<tr>
|
||||
<td colspan="{{response.values | length}}"
|
||||
class="header">
|
||||
Скорость ветра, м/с
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for value in response.values %}
|
||||
<td class="wind"
|
||||
style="background-color: rgba(128, 128, 128, {{value.wind_speed * 0.05}});">
|
||||
<span class="speed">{{value.wind_speed}}</span>
|
||||
{% if value.wind_gust != value.wind_speed %}
|
||||
<span class="gust">
|
||||
({{value.wind_gust}})
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<!-- precipitation -->
|
||||
<tr>
|
||||
<td colspan="{{response.values | length}}"
|
||||
class="header">
|
||||
Осадки, мм
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for value in response.values %}
|
||||
<td class="precipitation"
|
||||
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
|
||||
<span class="value">{{value.precipitation or ' '}}</span>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<!-- pressure -->
|
||||
<tr>
|
||||
<td colspan="{{response.values | length}}"
|
||||
class="header">
|
||||
Давление, мм рт. ст.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for value in response.values %}
|
||||
<td class="pressure">
|
||||
{% for pressure in value.pressure %}
|
||||
<div class="value"
|
||||
style="background-color: rgba(128, 0, 255, {{(pressure - 720) * 0.008}});">
|
||||
{{pressure}}</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<!-- humidity -->
|
||||
<tr>
|
||||
<td colspan="{{response.values | length}}"
|
||||
class="header">
|
||||
Влажность, %
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{% for value in response.values %}
|
||||
<td class="humidity"
|
||||
style="background-color: rgba(128, 128, 255, {{value.humidity * 0.005}});">
|
||||
<span class="value">{{value.humidity}}</span>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<script src="/static/index.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
34
gallery/logging.yaml
Normal file
34
gallery/logging.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
version: 1
|
||||
disable_existing_loggers: False
|
||||
formatters:
|
||||
default:
|
||||
# "()": uvicorn.logging.DefaultFormatter
|
||||
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
access:
|
||||
# "()": uvicorn.logging.AccessFormatter
|
||||
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
handlers:
|
||||
default:
|
||||
formatter: default
|
||||
class: logging.StreamHandler
|
||||
stream: ext://sys.stderr
|
||||
access:
|
||||
formatter: access
|
||||
class: logging.StreamHandler
|
||||
stream: ext://sys.stdout
|
||||
loggers:
|
||||
uvicorn.error:
|
||||
level: INFO
|
||||
handlers:
|
||||
- default
|
||||
propagate: no
|
||||
uvicorn.access:
|
||||
level: INFO
|
||||
handlers:
|
||||
- access
|
||||
propagate: no
|
||||
root:
|
||||
level: INFO
|
||||
handlers:
|
||||
- default
|
||||
propagate: no
|
||||
19
gallery/main.py
Normal file
19
gallery/main.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
|
||||
import uvicorn
|
||||
|
||||
from gallery.easel import build_app
|
||||
from gallery.painting.gismeteo.api import GismeteoApi
|
||||
|
||||
app = build_app(GismeteoApi())
|
||||
|
||||
|
||||
def run():
|
||||
uvicorn.run(
|
||||
"gallery.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
log_config=str(Path(__file__).parent / "logging.yaml"),
|
||||
reload="DEBUG" in environ,
|
||||
)
|
||||
0
gallery/painting/__init__.py
Normal file
0
gallery/painting/__init__.py
Normal file
0
gallery/painting/gismeteo/__init__.py
Normal file
0
gallery/painting/gismeteo/__init__.py
Normal file
96
gallery/painting/gismeteo/api.py
Normal file
96
gallery/painting/gismeteo/api.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import aiohttp
|
||||
from aiocache import cached
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from gallery.sketch.weather.api import WeatherApi
|
||||
from gallery.sketch.weather.model import WeatherResponse, WeatherValue
|
||||
|
||||
from . import datehelp
|
||||
from .parser import DAYS_PARSER, LOCATION_PARSER, ONE_DAY_PARSER, ROW_PARSERS
|
||||
|
||||
logger = logging.getLogger("gismeteo")
|
||||
|
||||
|
||||
class GismeteoApi(WeatherApi):
|
||||
BASE_URL = "https://www.gismeteo.ru"
|
||||
CACHE_TTL = 10 * 60
|
||||
|
||||
USER_AGENT = (
|
||||
"Mozilla/5.0 (X11; Linux x86_64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/126.0.0.0 Safari/537.36"
|
||||
)
|
||||
COOKIE = (
|
||||
"cf_clearance=U28mYVC0ENu88vorlL_CWmWOoevvXp0vb4xCqfqYC9s-"
|
||||
"1722273367-1.0.1.1-"
|
||||
"IDV73azTHY0V.NAnmEvok3zf5HHEkvF098pmya7IiqRRB5nk3FhbLCb0AeWm_kpTFqi1niFk2mYN_ramGTSl0A"
|
||||
)
|
||||
|
||||
async def _request(self, endpoint: str) -> str:
|
||||
url = f"{self.BASE_URL}/{endpoint}"
|
||||
logger.info(url)
|
||||
async with aiohttp.ClientSession(
|
||||
headers={
|
||||
"User-Agent": self.USER_AGENT,
|
||||
"Cookie": self.COOKIE,
|
||||
},
|
||||
raise_for_status=True,
|
||||
) as session:
|
||||
async with session.request("GET", url) as response:
|
||||
return await response.text()
|
||||
|
||||
def _parse_oneday(self, date: datetime.date, data: str) -> WeatherResponse:
|
||||
result: List[Dict[str, Any]] = []
|
||||
soup = BeautifulSoup(data, features="html.parser")
|
||||
location = LOCATION_PARSER.parse_location(data)
|
||||
widget = ONE_DAY_PARSER.parse_widget(soup)
|
||||
for parser in ROW_PARSERS:
|
||||
for index, value in enumerate(parser.parse_row(widget)):
|
||||
while len(result) < index + 1:
|
||||
result.append({})
|
||||
result[index][parser.KEY] = value
|
||||
values = [WeatherValue(**item) for item in result]
|
||||
return WeatherResponse(
|
||||
location=location or "n/a",
|
||||
date=date,
|
||||
period="day",
|
||||
values=values,
|
||||
)
|
||||
|
||||
def _parse_manydays(self, data: str) -> WeatherResponse:
|
||||
result: List[Dict[str, Any]] = []
|
||||
soup = BeautifulSoup(data, features="html.parser")
|
||||
location = LOCATION_PARSER.parse_location(data)
|
||||
widget = DAYS_PARSER.parse_widget(soup)
|
||||
for parser in ROW_PARSERS:
|
||||
for index, value in enumerate(parser.parse_row(widget)):
|
||||
while len(result) < index + 1:
|
||||
result.append({})
|
||||
result[index][parser.KEY] = value
|
||||
values = [WeatherValue(**item) for item in result]
|
||||
return WeatherResponse(
|
||||
location=location or "n/a",
|
||||
date=datetime.date.today(),
|
||||
period="days",
|
||||
values=values,
|
||||
)
|
||||
|
||||
async def get_locations(self) -> list[str]:
|
||||
return [
|
||||
"orel-4432",
|
||||
"zmiyevka-184640",
|
||||
]
|
||||
|
||||
@cached(ttl=CACHE_TTL)
|
||||
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
||||
data = await self._request(f"weather-{location_id}/{datehelp.dump(date)}")
|
||||
return self._parse_oneday(date, data)
|
||||
|
||||
@cached(ttl=CACHE_TTL)
|
||||
async def get_days(self, location_id: str, days: int) -> WeatherResponse:
|
||||
data = await self._request(f"weather-{location_id}/{days}-days")
|
||||
return self._parse_manydays(data)
|
||||
30
gallery/painting/gismeteo/core.py
Normal file
30
gallery/painting/gismeteo/core.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from typing import Generic, Iterable, Optional, TypeVar
|
||||
|
||||
from bs4 import Tag
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class WidgetParser:
|
||||
def parse_widget(self, tag: Tag) -> Tag:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BaseWidgetParser(WidgetParser):
|
||||
SELECT: str
|
||||
|
||||
def __init__(self, select: Optional[str] = None):
|
||||
super().__init__()
|
||||
self._select = select or self.SELECT
|
||||
|
||||
def parse_widget(self, tag: Tag) -> Tag:
|
||||
widget = tag.select_one(self._select)
|
||||
if widget is None:
|
||||
raise ValueError(self._select)
|
||||
return widget
|
||||
|
||||
|
||||
class RowParser(Generic[T]):
|
||||
KEY: str
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[T]:
|
||||
raise NotImplementedError
|
||||
39
gallery/painting/gismeteo/datehelp.py
Normal file
39
gallery/painting/gismeteo/datehelp.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import datetime
|
||||
from enum import Enum
|
||||
|
||||
import dateparser
|
||||
import dateparser.date_parser
|
||||
|
||||
|
||||
class Day(str, Enum):
|
||||
TODAY = "today"
|
||||
TOMORROW = "tomorrow"
|
||||
DAY_3 = "3-day"
|
||||
DAY_4 = "4-day"
|
||||
DAY_5 = "5-day"
|
||||
|
||||
|
||||
def parse(value: str) -> datetime.date:
|
||||
if value in ["today", "mock"]:
|
||||
return datetime.date.today()
|
||||
elif value == "tomorrow":
|
||||
return datetime.date.today() + datetime.timedelta(days=1)
|
||||
elif value.endswith("-day"):
|
||||
days = int(value.split("-")[0]) - 1
|
||||
return datetime.date.today() + datetime.timedelta(days=days)
|
||||
else:
|
||||
date = dateparser.parse(value)
|
||||
if date is None:
|
||||
raise ValueError(value)
|
||||
return date.date()
|
||||
|
||||
|
||||
def dump(date: datetime.date) -> str:
|
||||
today = datetime.date.today()
|
||||
days = (date - today).days
|
||||
if days == 0:
|
||||
return "today"
|
||||
elif days == 1:
|
||||
return "tomorrow"
|
||||
else:
|
||||
return f"{days + 1}-day"
|
||||
5
gallery/painting/gismeteo/mock/__init__.py
Normal file
5
gallery/painting/gismeteo/mock/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from gallery.sketch.mock import MockData
|
||||
|
||||
GISMETEO_MOCK_DATA = MockData(Path(__file__).parent / "data")
|
||||
5246
gallery/painting/gismeteo/mock/data/10-days.html
Normal file
5246
gallery/painting/gismeteo/mock/data/10-days.html
Normal file
File diff suppressed because one or more lines are too long
374
gallery/painting/gismeteo/mock/data/today.html
Normal file
374
gallery/painting/gismeteo/mock/data/today.html
Normal file
File diff suppressed because one or more lines are too long
186
gallery/painting/gismeteo/parser.py
Normal file
186
gallery/painting/gismeteo/parser.py
Normal file
@@ -0,0 +1,186 @@
|
||||
import datetime
|
||||
import re
|
||||
from typing import Iterable
|
||||
|
||||
import dateparser
|
||||
from bs4 import Tag
|
||||
|
||||
from gallery.sketch.weather.model import Cloudness, Precipitation, Sky, WindDirection
|
||||
|
||||
from .core import BaseWidgetParser, RowParser
|
||||
|
||||
ONE_DAY_PARSER = BaseWidgetParser(".widget.widget-oneday .widget-items")
|
||||
DAYS_PARSER = BaseWidgetParser(".widget.widget-days .widget-items")
|
||||
|
||||
|
||||
class LocationParser:
|
||||
PATTERN = re.compile('{"ru":{"city":{"name":"(.*?)"')
|
||||
|
||||
def parse_location(self, data: str) -> str | None:
|
||||
match = self.PATTERN.search(data)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
LOCATION_PARSER = LocationParser()
|
||||
|
||||
|
||||
class DateParser(RowParser[datetime.datetime]):
|
||||
KEY = "date"
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[datetime.datetime]:
|
||||
datetime_date_tag = tag.select_one(
|
||||
".widget-row.widget-row-datetime-date > .row-item"
|
||||
)
|
||||
if datetime_date_tag:
|
||||
date_str = datetime_date_tag.find(text=True, recursive=False).text
|
||||
date = dateparser.parse(date_str, languages=["ru"])
|
||||
for item in tag.select(".widget-row.widget-row-datetime-time > .row-item"):
|
||||
time_str = item.text
|
||||
time = dateparser.parse(time_str, languages=["ru"])
|
||||
time = time.replace(year=date.year, month=date.month, day=date.day)
|
||||
yield time
|
||||
else:
|
||||
for item in tag.select(".widget-row.widget-row-date > .row-item"):
|
||||
date_str = item.text
|
||||
date = dateparser.parse(date_str, languages=["ru"])
|
||||
yield date
|
||||
|
||||
|
||||
class SkyParser(RowParser[Sky]):
|
||||
KEY = "sky"
|
||||
|
||||
CLOUDNESS_MAP: dict[str, Cloudness] = {
|
||||
"ясно": Cloudness.CLEAR,
|
||||
"малооблачно": Cloudness.PARTLY_CLOUDY,
|
||||
"облачно": Cloudness.CLOUDY,
|
||||
"пасмурно": Cloudness.MAINLY_CLOUDY,
|
||||
}
|
||||
|
||||
PRECIPITATION_MAP: dict[str, Precipitation] = {
|
||||
"без осадков": Precipitation.NO,
|
||||
"небольшой дождь": Precipitation.SMALL_RAIN,
|
||||
"дождь": Precipitation.RAIN,
|
||||
"ливень": Precipitation.SHOWER,
|
||||
}
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[Sky]:
|
||||
for item in tag.select(".widget-row[data-row=icon-tooltip] > .row-item"):
|
||||
sky_str = item.attrs["data-tooltip"]
|
||||
values = {item.strip().lower() for item in sky_str.split(",")}
|
||||
cloudness = Cloudness.CLEAR
|
||||
precipitation = Precipitation.NO
|
||||
thunder = "гроза" in values
|
||||
fog = "дымка" in values
|
||||
for k, v in self.CLOUDNESS_MAP.items():
|
||||
if k in values:
|
||||
cloudness = v
|
||||
break
|
||||
for k, v in self.PRECIPITATION_MAP.items():
|
||||
if k in values:
|
||||
precipitation = v
|
||||
break
|
||||
yield Sky(
|
||||
cloudness=cloudness,
|
||||
precipitation=precipitation,
|
||||
thunder=thunder,
|
||||
fog=fog,
|
||||
)
|
||||
|
||||
|
||||
class TemperatureParser(RowParser[list[int]]):
|
||||
KEY = "temperature"
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[list[int]]:
|
||||
for item in tag.select(
|
||||
".widget-row-chart[data-row=temperature-air] > .chart > .values > .value"
|
||||
):
|
||||
yield [
|
||||
int(value.attrs["value"]) for value in item.select("temperature-value")
|
||||
]
|
||||
|
||||
|
||||
class WindSpeedParser(RowParser[int]):
|
||||
KEY = "wind_speed"
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[int]:
|
||||
for item in tag.select(
|
||||
".widget-row[data-row=wind-speed] > .row-item > speed-value"
|
||||
):
|
||||
yield int(item.attrs["value"])
|
||||
|
||||
|
||||
class WindGustParser(RowParser[int]):
|
||||
KEY = "wind_gust"
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[int]:
|
||||
for item in tag.select(".widget-row[data-row=wind-gust] > .row-item"):
|
||||
value = item.select_one("speed-value")
|
||||
yield int(value.attrs["value"]) if value else 0
|
||||
|
||||
|
||||
class WindDirectionParser(RowParser[WindDirection]):
|
||||
KEY = "wind_direction"
|
||||
|
||||
WIND_DIRECTION_MAP: dict[str, WindDirection] = {
|
||||
"штиль": WindDirection.CALM,
|
||||
"с": WindDirection.N,
|
||||
"св": WindDirection.NO,
|
||||
"в": WindDirection.O,
|
||||
"юв": WindDirection.SO,
|
||||
"ю": WindDirection.S,
|
||||
"юз": WindDirection.SW,
|
||||
"з": WindDirection.W,
|
||||
"сз": WindDirection.NW,
|
||||
}
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[WindDirection]:
|
||||
for item in tag.select(
|
||||
".widget-row[data-row=wind-direction] > .row-item > .direction"
|
||||
):
|
||||
wind_direction_str = item.text.lower()
|
||||
yield self.WIND_DIRECTION_MAP[wind_direction_str]
|
||||
|
||||
|
||||
class WindPrecipitationParser(RowParser[float]):
|
||||
KEY = "precipitation"
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[float]:
|
||||
for item in tag.select(
|
||||
".widget-row[data-row=precipitation-bars] > .row-item > .item-unit"
|
||||
):
|
||||
yield float(item.text.replace(",", "."))
|
||||
|
||||
|
||||
class PressureParser(RowParser[list[int]]):
|
||||
KEY = "pressure"
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[list[int]]:
|
||||
for item in tag.select(
|
||||
".widget-row-chart[data-row=pressure] > .chart > .values > .value"
|
||||
):
|
||||
yield [int(value.attrs["value"]) for value in item.select("pressure-value")]
|
||||
|
||||
|
||||
class HumidityParser(RowParser[int]):
|
||||
KEY = "humidity"
|
||||
|
||||
def parse_row(self, tag: Tag) -> Iterable[int]:
|
||||
for item in tag.select(".widget-row[data-row=humidity] > .row-item"):
|
||||
yield int(item.text)
|
||||
|
||||
|
||||
ROW_PARSERS: list[RowParser] = [
|
||||
DateParser(),
|
||||
SkyParser(),
|
||||
TemperatureParser(),
|
||||
WindSpeedParser(),
|
||||
WindGustParser(),
|
||||
WindDirectionParser(),
|
||||
WindPrecipitationParser(),
|
||||
PressureParser(),
|
||||
HumidityParser(),
|
||||
]
|
||||
|
||||
ROW_PARSERS_MAP: dict[str, RowParser] = {parser.KEY: parser for parser in ROW_PARSERS}
|
||||
15
gallery/sketch/mock.py
Normal file
15
gallery/sketch/mock.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class MockData:
|
||||
|
||||
def __init__(self, data_dir) -> None:
|
||||
self._data_dir = data_dir
|
||||
|
||||
def get_html(self, key: str) -> str:
|
||||
return (self._data_dir / f"{key}.html").read_text()
|
||||
|
||||
def get_json(self, key: str) -> dict:
|
||||
data = json.loads((Path(__file__).parent / f"data/{key}.json").read_text())
|
||||
return data
|
||||
0
gallery/sketch/schedule/__init_.py
Normal file
0
gallery/sketch/schedule/__init_.py
Normal file
26
gallery/sketch/schedule/model.py
Normal file
26
gallery/sketch/schedule/model.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class Channel(Model):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class ScheduleItem(Model):
|
||||
start: datetime.datetime
|
||||
end: datetime.datetime
|
||||
label: str
|
||||
category: str | None
|
||||
|
||||
|
||||
class Schedule(Model):
|
||||
channel: Channel
|
||||
date: datetime.date
|
||||
items: list[ScheduleItem]
|
||||
0
gallery/sketch/weather/__init__.py
Normal file
0
gallery/sketch/weather/__init__.py
Normal file
14
gallery/sketch/weather/api.py
Normal file
14
gallery/sketch/weather/api.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import datetime
|
||||
|
||||
from .model import WeatherResponse
|
||||
|
||||
|
||||
class WeatherApi:
|
||||
async def get_locations(self) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_days(self, location_id: str, days: int) -> WeatherResponse:
|
||||
raise NotImplementedError
|
||||
12
gallery/sketch/weather/mock/__init__.py
Normal file
12
gallery/sketch/weather/mock/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
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
gallery/sketch/weather/mock/data/day.json
Normal file
1
gallery/sketch/weather/mock/data/day.json
Normal file
@@ -0,0 +1 @@
|
||||
{"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
gallery/sketch/weather/mock/data/days.json
Normal file
1
gallery/sketch/weather/mock/data/days.json
Normal file
@@ -0,0 +1 @@
|
||||
{"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}]}
|
||||
61
gallery/sketch/weather/model.py
Normal file
61
gallery/sketch/weather/model.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import datetime
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Model(BaseModel):
|
||||
class Config:
|
||||
use_enum_values = True
|
||||
|
||||
|
||||
class Cloudness(str, Enum):
|
||||
CLEAR = "clear"
|
||||
PARTLY_CLOUDY = "party_cloudy"
|
||||
CLOUDY = "cloudy"
|
||||
MAINLY_CLOUDY = "mainly_cloudy"
|
||||
|
||||
|
||||
class Precipitation(str, Enum):
|
||||
NO = "no"
|
||||
SMALL_RAIN = "small_rain"
|
||||
RAIN = "rain"
|
||||
SHOWER = "shower"
|
||||
|
||||
|
||||
class Sky(Model):
|
||||
cloudness: Cloudness
|
||||
precipitation: Precipitation
|
||||
thunder: bool
|
||||
fog: bool
|
||||
|
||||
|
||||
class WindDirection(str, Enum):
|
||||
CALM = "calm"
|
||||
N = "N"
|
||||
NO = "NO"
|
||||
O = "O"
|
||||
SO = "SO"
|
||||
S = "S"
|
||||
SW = "SW"
|
||||
W = "W"
|
||||
NW = "NW"
|
||||
|
||||
|
||||
class WeatherValue(Model):
|
||||
date: datetime.datetime
|
||||
sky: Sky
|
||||
temperature: list[int]
|
||||
wind_speed: int
|
||||
wind_gust: int
|
||||
wind_direction: WindDirection
|
||||
precipitation: float
|
||||
pressure: list[int]
|
||||
humidity: int
|
||||
|
||||
|
||||
class WeatherResponse(Model):
|
||||
location: str
|
||||
date: datetime.date
|
||||
period: str
|
||||
values: list[WeatherValue]
|
||||
22
gallery/sketch/weather/util.py
Normal file
22
gallery/sketch/weather/util.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import datetime
|
||||
|
||||
from .model import Cloudness, Precipitation, Sky, WeatherValue, WindDirection
|
||||
|
||||
|
||||
def build_weather_value(date: datetime.datetime) -> WeatherValue:
|
||||
return WeatherValue(
|
||||
date=date,
|
||||
sky=Sky(
|
||||
cloudness=Cloudness.CLEAR,
|
||||
precipitation=Precipitation.NO,
|
||||
thunder=False,
|
||||
fog=False,
|
||||
),
|
||||
temperature=[],
|
||||
wind_speed=0,
|
||||
wind_gust=0,
|
||||
wind_direction=WindDirection.CALM,
|
||||
precipitation=0,
|
||||
pressure=[],
|
||||
humidity=0,
|
||||
)
|
||||
Reference in New Issue
Block a user