Last active
July 21, 2023 12:15
-
-
Save tomschr/695e4a04d50ef129285ae70f0c6f628a to your computer and use it in GitHub Desktop.
Proof-of-concept to retrieve (GET) and store (POST) ratings from documentation URLs similar to doc.suse.com
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
Proof-of-concept to retrieve (GET) and store (POST) ratings from | |
documentation URLs similar to doc.suse.com. | |
Requirements | |
------------ | |
* aiohttp | |
* Python >=3.6, preferably a more recent version | |
Retrieve information from database | |
---------------------------------- | |
* Return JSON object: | |
$ curl http://localhost:8080/sle-ha/15-GA/single-html/SLE-HA-pmremote-quick/ | |
{"rate": 5} | |
Store information into database | |
------------------------------- | |
$ curl -X POST -H "Content-Type: application/json" \ | |
-d '{"rate": 5}' \ | |
http://localhost:8080/sle-ha/15-SP1/html/SLE-HA-all/foo.html | |
$ curl http://localhost:8080/sle-ha/15-SP1/html/SLE-HA-all/foo.html | |
{"rate": 5} | |
TODOs | |
----- | |
* Connect to a real database | |
* Improve logging | |
* Correct error codes when something goes wrong | |
* Allow application/x-www-form-urlencoded" as content type? | |
* Be relaxed when using double slashes (http://localhost:8080//... vs. http://localhost:8080/...) | |
* Detect SUMA URLs | |
See also | |
-------- | |
* aiohttp documentation: https://docs.aiohttp.org | |
* curl POST examples: https://gist.github.com/subfuzion/08c5d85437d5d4f00e58 | |
""" | |
from aiohttp import web | |
import json | |
import logging | |
from typing import Dict, Union, Optional | |
logging.basicConfig(level=logging.DEBUG) | |
routes = web.RouteTableDef() | |
AVAILABLE_PRODUCTS = ("sle-ha", "sle-hpc", "sles", "suma", "slesforsap", ...) | |
DATABASE = [ | |
# The keys are a moving target and to be defined. | |
# Theoretically, we don't need product, release, | |
# and sp. It was just from a former idea so I left it to have some "flesh". | |
# | |
# We save only the part of the part of the URL after the host name to | |
# make comparison a bit easier and avoid host name variations | |
dict( | |
product="sle-hpc", | |
release=15, | |
sp=3, | |
rate=10, | |
url="/sle-hpc/15-SP3/html/hpc-guide/cha-slurm.html", | |
), | |
dict( | |
product="sle-ha", | |
release=15, | |
sp=0, | |
rate=5, | |
url="/sle-ha/15-GA/single-html/SLE-HA-pmremote-quick/", | |
), | |
dict( | |
product="sle-ha", | |
release=15, | |
sp=0, | |
rate=2, | |
url="/sle-ha/15-GA/html/SLE-HA-all/art-sleha-pmremote-quick.html", | |
), | |
] | |
def search_in_database(url: Union[str, yarl.URL]) -> Dict[str, Optional[int]]: | |
""" | |
Search in the database for given URL | |
:param url: the URL | |
""" | |
for item in DATABASE: | |
u = item["url"] | |
if u == str(url): | |
return item | |
return dict(rate=None) | |
@routes.get(r"/{product}/{release}/html/{guide}/{topic}") | |
@routes.get(r"/{product}/{release}/single-html/{guide}/") | |
# /sle-ha/15-GA/single-html/SLE-HA-pmremote-quick/ | |
# /external-tree/en-us/suma/4.1/suse-manager/installation/install-intro.html | |
# /external-tree/en-us/suma/4.0/suse-manager/retail/retail-introduction.html | |
# /sle-pos/11-SP3/html/SLEPOS-imgsrv12/index.html | |
async def get_rating(request: web.Request) -> web.StreamResponse: | |
""" | |
Async function when a GET event happen | |
""" | |
product = request.match_info.get("product") | |
release = request.match_info.get("release") | |
guide = request.match_info.get("guide") | |
topic = request.match_info.get("topic") | |
# see https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.Request | |
url = request.rel_url | |
found = search_in_database(str(url)) | |
data = {"rate": found.get("rate")} | |
return web.json_response(data) | |
# another alternative would be simple text: | |
# return web.Response(text=f"Found rating {found['rate']}", content_type="text/plain") | |
@routes.post(r"/{product}/{release}/html/{guide}/{topic}") | |
@routes.post(r"/{product}/{release}/single-html/{guide}/") | |
async def post_rateing(request: web.Request) -> web.StreamResponse: | |
""" | |
Async function called when a POST event happen. | |
If every parameter was okay, rating is included into the database. | |
""" | |
# The product, release guide, and topic are not needed, but it is | |
# "required" to form the URL. | |
# Haven't found an easy way to make a "get everything after /" request | |
# You can extract the variable with, for example: | |
# product = request.match_info.get("product", None) | |
url = str(request.rel_url) | |
if request.content_type != "application/json": | |
return web.HTTPNotAcceptable( | |
text=f"Unsupported content_type {request.content_type}" | |
) | |
# Check if we got the right product abbreviation: | |
if product not in AVAILABLE_PRODUCTS: | |
return web.HTTPNotAcceptable(text=f"product {product!r} not known") | |
rating = await request.json() | |
print(f"Received {rating}") | |
if rating.get("rate") is None: | |
return web.HTTPNotAcceptable(text=f"Rating not provided") | |
try: | |
rate = rating.get("rate") | |
rate = int(rate) | |
except ValueError: | |
return web.HTTPNotAcceptable( | |
text=f"Invalid rating, expected a number, but got {rate!r}" | |
) | |
# All is good, so include it into our database: | |
data = dict(product=product, release=release, url=url, rate=rate) | |
DATABASE.append(data) | |
return web.Response(text=f"Added new rating entry with {data}") | |
async def on_shutdown(app: web.Application) -> None: | |
print("\n*** Closing server...") | |
def main(): | |
""" | |
Main entry point | |
""" | |
app = web.Application() | |
app.add_routes(routes) | |
app.on_shutdown.append(on_shutdown) | |
return web.run_app(app) | |
if __name__ == "__main__": | |
print("DATABASE:", DATABASE) | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment