refactor(weather): add app_build method
This commit is contained in:
@@ -21,4 +21,4 @@ COPY --from=builder /app ./
|
|||||||
COPY gismeteo gismeteo/
|
COPY gismeteo gismeteo/
|
||||||
COPY weather weather/
|
COPY weather weather/
|
||||||
|
|
||||||
CMD ["uvicorn", "weather.app:app", "--host", "0.0.0.0", "--port", "80"]
|
CMD ["uvicorn", "weather.main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
import dateparser
|
import dateparser
|
||||||
import dateparser.date_parser
|
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:
|
def parse(value: str) -> datetime.date:
|
||||||
if value == "today" or value == "mock":
|
if value == "today" or value == "mock":
|
||||||
return datetime.date.today()
|
return datetime.date.today()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ requires = ["poetry-core"]
|
|||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
app = 'weather.app:run'
|
app = 'weather.main:run'
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-p no:warnings"
|
addopts = "-p no:warnings"
|
||||||
|
|||||||
@@ -6,10 +6,3 @@ from .model import WeatherResponse
|
|||||||
class WeatherApi:
|
class WeatherApi:
|
||||||
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_API: WeatherApi = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_api() -> WeatherApi:
|
|
||||||
return DEFAULT_API
|
|
||||||
|
|||||||
@@ -1,27 +1,22 @@
|
|||||||
import locale
|
import locale
|
||||||
from os import environ
|
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
from gismeteo.api import GismeteoApi
|
from weather.api import WeatherApi
|
||||||
|
|
||||||
from . import api as _api
|
|
||||||
from .route import api, doc, view
|
from .route import api, doc, view
|
||||||
|
|
||||||
_api.DEFAULT_API = GismeteoApi()
|
|
||||||
|
|
||||||
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
|
def build_app(weather_api: WeatherApi) -> FastAPI:
|
||||||
|
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="Weather",
|
title="Weather",
|
||||||
docs_url=None,
|
docs_url=None,
|
||||||
redoc_url=None,
|
redoc_url=None,
|
||||||
)
|
)
|
||||||
doc.mount(app)
|
app.state.weather_api = weather_api
|
||||||
api.mount(app)
|
doc.mount(app)
|
||||||
view.mount(app)
|
api.mount(app)
|
||||||
|
view.mount(app)
|
||||||
|
return app
|
||||||
def run():
|
|
||||||
uvicorn.run("weather.app:app", host="0.0.0.0", port=8000, reload="DEBUG" in environ)
|
|
||||||
|
|||||||
12
weather/main.py
Normal file
12
weather/main.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from os import environ
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from gismeteo.api import GismeteoApi
|
||||||
|
from weather.app import build_app
|
||||||
|
|
||||||
|
app = build_app(GismeteoApi())
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
uvicorn.run("weather.main:app", host="0.0.0.0", port=8000, reload="DEBUG" in environ)
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI, Request
|
||||||
|
|
||||||
from weather.api import get_api
|
|
||||||
from weather.model import WeatherResponse
|
from weather.model import WeatherResponse
|
||||||
|
|
||||||
|
|
||||||
def mount(app: FastAPI):
|
def mount(app: FastAPI):
|
||||||
@app.get("/api/weather/{location}/{date}")
|
@app.get("/api/weather/{location}/{date}")
|
||||||
async def get_weather(location: str, date: datetime.date) -> WeatherResponse:
|
async def get_weather_api(
|
||||||
return await get_api().get_day(location, date)
|
request: Request, location: str, date: datetime.date
|
||||||
|
) -> WeatherResponse:
|
||||||
|
return await request.app.state.weather_api.get_day(location, date)
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ 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 gismeteo import datehelp
|
||||||
|
from gismeteo.location import LOCATION_BUNDLE
|
||||||
from gismeteo.mock import MOCK_DATA
|
from gismeteo.mock import MOCK_DATA
|
||||||
from weather.api import get_api
|
from weather.model import WeatherResponse
|
||||||
|
|
||||||
from .filters import cloudness_icon, wind_direction_icon
|
from .filters import cloudness_icon, wind_direction_icon
|
||||||
|
|
||||||
@@ -19,6 +21,30 @@ def mount(app: FastAPI):
|
|||||||
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
templates.env.filters["wind_direction_icon"] = wind_direction_icon
|
||||||
templates.env.filters["cloudness_icon"] = cloudness_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={
|
||||||
|
"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):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
request=request,
|
||||||
|
name="index.html",
|
||||||
|
context={
|
||||||
|
"locations": LOCATION_BUNDLE._values,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@app.get("/weather/{location}", response_class=RedirectResponse)
|
@app.get("/weather/{location}", response_class=RedirectResponse)
|
||||||
async def get_weather_default(location: str):
|
async def get_weather_default(location: str):
|
||||||
return RedirectResponse(f"{location}/{datetime.date.today()}")
|
return RedirectResponse(f"{location}/{datetime.date.today()}")
|
||||||
@@ -26,23 +52,15 @@ def mount(app: FastAPI):
|
|||||||
@app.get("/weather/{location}/mock", response_class=HTMLResponse)
|
@app.get("/weather/{location}/mock", response_class=HTMLResponse)
|
||||||
async def get_weather_mock(request: Request):
|
async def get_weather_mock(request: Request):
|
||||||
response = MOCK_DATA.response
|
response = MOCK_DATA.response
|
||||||
return templates.TemplateResponse(
|
return build_weather_response(request, response)
|
||||||
request=request,
|
|
||||||
name="weather.html",
|
# @app.get("/weather/{location}/{day}", response_class=HTMLResponse)
|
||||||
context={
|
async def get_weather_day(request: Request, location: str, day: datehelp.Day):
|
||||||
"datetime": datetime,
|
date = datehelp.parse(day)
|
||||||
"response": response,
|
response = await request.app.state.weather_api.get_day(location, date)
|
||||||
},
|
return build_weather_response(request, response)
|
||||||
)
|
|
||||||
|
|
||||||
@app.get("/weather/{location}/{date}", response_class=HTMLResponse)
|
@app.get("/weather/{location}/{date}", response_class=HTMLResponse)
|
||||||
async def get_weather(request: Request, location: str, date: datetime.date):
|
async def get_weather(request: Request, location: str, date: datetime.date):
|
||||||
response = await get_api().get_day(location, date)
|
response = await request.app.state.weather_api.get_day(location, date)
|
||||||
return templates.TemplateResponse(
|
return build_weather_response(request, response)
|
||||||
request=request,
|
|
||||||
name="weather.html",
|
|
||||||
context={
|
|
||||||
"datetime": datetime,
|
|
||||||
"response": response,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ from weather.model import Cloudness, Precipitation, Sky, WindDirection
|
|||||||
|
|
||||||
def wind_direction_icon(wind_direction: WindDirection) -> str:
|
def wind_direction_icon(wind_direction: WindDirection) -> str:
|
||||||
return {
|
return {
|
||||||
WindDirection.N: "🡫",
|
WindDirection.N: "⬇️",
|
||||||
WindDirection.NO: "🡯",
|
WindDirection.NO: "↙️",
|
||||||
WindDirection.O: "🡨",
|
WindDirection.O: "⬅️",
|
||||||
WindDirection.SO: "🡬",
|
WindDirection.SO: "↖️",
|
||||||
WindDirection.S: "🡡",
|
WindDirection.S: "⬆️",
|
||||||
WindDirection.SW: "🡭",
|
WindDirection.SW: "↗️",
|
||||||
WindDirection.W: "🡪",
|
WindDirection.W: "➡️",
|
||||||
WindDirection.NW: "🡦",
|
WindDirection.NW: "↘️",
|
||||||
WindDirection.CALM: "",
|
WindDirection.CALM: "",
|
||||||
}.get(wind_direction, wind_direction)
|
}.get(wind_direction, wind_direction)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ a.button {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: gray;
|
color: gray;
|
||||||
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
|||||||
26
weather/route/view/templates/index.html
Normal file
26
weather/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">
|
||||||
|
<link rel="icon"
|
||||||
|
href="/static/favicon.ico"
|
||||||
|
type="image/x-icon">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ul>
|
||||||
|
{% for location in locations %}
|
||||||
|
<li><a href="weather/{{location.name}}-{{location.location_id}}">{{location.name}}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -2,151 +2,151 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport"
|
<meta name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0">
|
content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible"
|
<meta http-equiv="X-UA-Compatible"
|
||||||
content="ie=edge">
|
content="ie=edge">
|
||||||
<title>Погода | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</title>
|
<title>Погода | {{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</title>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="/static/style.css">
|
href="/static/style.css">
|
||||||
<link rel="icon"
|
<link rel="icon"
|
||||||
href="/static/favicon.ico"
|
href="/static/favicon.ico"
|
||||||
type="image/x-icon">
|
type="image/x-icon">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h3>
|
<h3>
|
||||||
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
<a class="button {{'disabled' if response.date == datetime.date.today() else ''}}"
|
||||||
href="{{response.date - datetime.timedelta(days=1)}}">🡨</a>
|
href="{{response.date - datetime.timedelta(days=1)}}">⬅️</a>
|
||||||
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
<span>{{response.location}} | {{response.date.strftime('%a, %d %B %Y')}}</span>
|
||||||
<a class="button"
|
<a class="button"
|
||||||
href="{{response.date + datetime.timedelta(days=1)}}">🡪</a>
|
href="{{response.date + datetime.timedelta(days=1)}}">➡️</a>
|
||||||
</h3>
|
</h3>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<!-- date -->
|
<!-- date -->
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<td
|
<td
|
||||||
class="date {{'now' if value.date < datetime.datetime.now() and value.date + datetime.timedelta(hours=3) > datetime.datetime.now() else ''}}">
|
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>
|
<span class="value">{{value.date.strftime('%H:%M')}}</span>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- cloudness -->
|
<!-- cloudness -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{response.values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Облачность
|
Облачность
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<td class="cloudness">
|
<td class="cloudness">
|
||||||
{% for icon in value.sky | cloudness_icon %}
|
{% for icon in value.sky | cloudness_icon %}
|
||||||
<div class="icon">{{icon}}</div>
|
<div class="icon">{{icon}}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- temperature -->
|
<!-- temperature -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{response.values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Температура, °C
|
Температура, °C
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<td class="temperature {{'positive' if value.temperature > 0 else 'negative'}}"
|
<td class="temperature {{'positive' if value.temperature > 0 else 'negative'}}"
|
||||||
style="background-color: rgba(255, 128, 128, {{(value.temperature - 10) * 0.015}});">
|
style="background-color: rgba(255, 128, 128, {{(value.temperature - 10) * 0.015}});">
|
||||||
<span class="value">{{value.temperature}}</span>
|
<span class="value">{{value.temperature}}</span>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- wind_direction -->
|
<!-- wind_direction -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{response.values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Направление ветра
|
Направление ветра
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<td class="wind">
|
<td class="wind">
|
||||||
<span class="icon">{{value.wind_direction | wind_direction_icon}}</span>
|
<span class="icon">{{value.wind_direction | wind_direction_icon}}</span>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- wind_speed -->
|
<!-- wind_speed -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{response.values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Скорость ветра, м/с
|
Скорость ветра, м/с
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<td class="wind"
|
<td class="wind"
|
||||||
style="background-color: rgba(128, 128, 128, {{value.wind_speed * 0.05}});">
|
style="background-color: rgba(128, 128, 128, {{value.wind_speed * 0.05}});">
|
||||||
<span class="speed">{{value.wind_speed}}</span>
|
<span class="speed">{{value.wind_speed}}</span>
|
||||||
{% if value.wind_gust != value.wind_speed %}
|
{% if value.wind_gust != value.wind_speed %}
|
||||||
<span class="gust">
|
<span class="gust">
|
||||||
({{value.wind_gust}})
|
({{value.wind_gust}})
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- precipitation -->
|
<!-- precipitation -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{response.values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Осадки, мм
|
Осадки, мм
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<td class="precipitation"
|
<td class="precipitation"
|
||||||
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
|
style="background-color: rgba(0, 128, 255, {{value.precipitation * 0.1}});">
|
||||||
<span class="value">{{value.precipitation or ' '}}</span>
|
<span class="value">{{value.precipitation or ' '}}</span>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- pressure -->
|
<!-- pressure -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{response.values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Давление, мм рт. ст.
|
Давление, мм рт. ст.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<td class="pressure"
|
<td class="pressure"
|
||||||
style="background-color: rgba(128, 0, 255, {{(value.pressure - 720) * 0.008}});">
|
style="background-color: rgba(128, 0, 255, {{(value.pressure - 720) * 0.008}});">
|
||||||
<span class="value">{{value.pressure}}</span>
|
<span class="value">{{value.pressure}}</span>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
<!-- humidity -->
|
<!-- humidity -->
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="{{response.values | length}}"
|
<td colspan="{{response.values | length}}"
|
||||||
class="header">
|
class="header">
|
||||||
Влажность, %
|
Влажность, %
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
{% for value in response.values %}
|
{% for value in response.values %}
|
||||||
<td class="humidity"
|
<td class="humidity"
|
||||||
style="background-color: rgba(128, 128, 255, {{value.humidity * 0.005}});">
|
style="background-color: rgba(128, 128, 255, {{value.humidity * 0.005}});">
|
||||||
<span class="value">{{value.humidity}}</span>
|
<span class="value">{{value.humidity}}</span>
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<script src="/static/index.js"></script>
|
<script src="/static/index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user