-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
173 lines (143 loc) · 5.33 KB
/
main.py
File metadata and controls
173 lines (143 loc) · 5.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
from decimal import Decimal, ROUND_UP
from typing import Annotated
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.responses import HTMLResponse, JSONResponse, Response
from fastapi import FastAPI, Request, Form, Query, APIRouter, Body, HTTPException
from pint import UnitRegistry
from pint.errors import DimensionalityError, UndefinedUnitError
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="html")
units = sorted([str(unit).replace("_", " ") for unit in UnitRegistry()])
@app.get("/")
async def index(request: Request) -> Response:
return templates.TemplateResponse(request=request, name="index.html")
hx_description = """You would not usually access this API directly.
All of the documentation below are read-only.
The use of `HX-Request` header is also **required** for requests to work.
\nTo see the web app in action, visit the [homepage](/)."""
hx = FastAPI(
title="MeasureMint HTMX API",
summary="MeasureMint's HTMX API Documentation. For developer use only.",
description=hx_description,
# TODO: get latest git tag
version="0.1.0",
swagger_ui_parameters={
"supportedSubmitMethods": [],
"defaultModelsExpandDepth": -1,
},
docs_url=None,
redoc_url=None,
)
@hx.get("/docs", include_in_schema=False)
async def hx_docs(request: Request) -> HTMLResponse:
root_path = request.scope.get("root_path").rstrip("/")
return get_swagger_ui_html(
openapi_url=f"{root_path}{hx.openapi_url}",
title=f"{hx.title} | Swagger UI",
swagger_favicon_url="/static/favicon/favicon.ico",
swagger_ui_parameters=hx.swagger_ui_parameters,
)
@hx.middleware("http")
async def check_hx_request(request: Request, call_next):
if request.headers.get("HX-Request"):
return await call_next(request)
stripped = request.url.path.strip("/hx/")
if stripped in ["docs", "openapi.json"]:
return await call_next(request)
return Response("Cannot process this request", status_code=403)
@hx.get("/suggestions", response_class=HTMLResponse)
async def suggest_units(
request: Request,
from_unit: Annotated[str | None, Query()] = None,
to_unit: Annotated[str | None, Query()] = None,
) -> HTMLResponse:
"""
Returns a list of units that match the query.
List of units come from [pint](https://pint.readthedocs.io/en/stable/).
\nThese are used by the unit inputs in the main form.
"""
value = from_unit or to_unit
if value:
filtered = [*filter(lambda u: value in u, units)][:10]
else:
filtered = units[:10]
if not len(filtered):
filtered = ["No units found"]
context = {"units": filtered}
return templates.TemplateResponse(
request=request, name="suggestions.html", context=context
)
@hx.post("/convert", response_class=HTMLResponse)
async def convert(
request: Request,
quantity: Annotated[float, Form()],
from_unit: Annotated[str, Form()],
to_unit: Annotated[str, Form()],
) -> HTMLResponse:
"""
Converts `quantity` from `from_unit` to `to_unit`.
validation of units is handled by
[pint](https://pint.readthedocs.io/en/stable/).
\nThis takes action when the form is submitted.
"""
from_unit = from_unit.replace(" ", "_")
to_unit = to_unit.replace(" ", "_")
try:
result = UnitRegistry().Quantity(quantity, from_unit).to(to_unit)
result = Decimal(str(result.magnitude))
result = result.quantize(Decimal("0.0001"), rounding=ROUND_UP)
except UndefinedUnitError as e:
unit = e.args[0]
message = f"'{unit}' is not a valid unit!"
context = {"message": message}
if from_unit == unit:
context["from_unit"] = unit
if to_unit == unit:
context["to_unit"] = unit
return templates.TemplateResponse(
request=request,
name="error.html",
context=context,
status_code=422,
)
except DimensionalityError:
message = f"Converting {from_unit} to {to_unit} is not possible!"
return templates.TemplateResponse(
request=request,
name="error.html",
context={"message": message},
status_code=422,
)
except Exception as e:
return templates.TemplateResponse(
request=request,
name="error.html",
context={"message": str(e)},
status_code=500,
)
context = {"result": result}
return templates.TemplateResponse(
request=request, name="convert.html", context=context
)
json = APIRouter(prefix="/json")
@json.post("/convert")
async def convert_json(
request: Request,
quantity: Annotated[float, Body()],
from_unit: Annotated[str, Body()],
to_unit: Annotated[str, Body()],
) -> JSONResponse:
try:
result = UnitRegistry().Quantity(quantity, from_unit).to(to_unit)
result = Decimal(str(result.magnitude))
result = result.quantize(Decimal("0.0001"), rounding=ROUND_UP)
except Exception as e:
raise HTTPException(
status_code=422, detail=f"Error while converting: {str(e)}"
) from e
return {"result": result}
app.mount("/hx", hx)
app.include_router(json)