Skip to content

Instantly share code, notes, and snippets.

@tomschr
Last active July 21, 2023 12:15
Show Gist options
  • Select an option

  • Save tomschr/695e4a04d50ef129285ae70f0c6f628a to your computer and use it in GitHub Desktop.

Select an option

Save tomschr/695e4a04d50ef129285ae70f0c6f628a to your computer and use it in GitHub Desktop.

Revisions

  1. tomschr revised this gist Aug 4, 2021. 1 changed file with 6 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions docrate.py
    Original file line number Diff line number Diff line change
    @@ -56,6 +56,12 @@

    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,
  2. tomschr revised this gist Aug 4, 2021. 1 changed file with 4 additions and 25 deletions.
    29 changes: 4 additions & 25 deletions docrate.py
    Original file line number Diff line number Diff line change
    @@ -93,18 +93,6 @@ def search_in_database(url: Union[str, yarl.URL]) -> Dict[str, Optional[int]]:
    return dict(rate=None)


    # Only for test purposes:
    @routes.get("/")
    @routes.get("/{name}")
    async def handle(request: web.Request) -> web.StreamResponse:
    """
    Example
    """
    name = request.match_info.get("name", "Anonymous")
    text = f"""Hello, {name}"""
    return web.Response(text=text)


    @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/
    @@ -122,14 +110,6 @@ async def get_rating(request: web.Request) -> web.StreamResponse:

    # see https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.Request
    url = request.rel_url
    print(" rel_url=", url, dir(url))
    print(" fragment=", url.fragment)
    print(" raw_parts=", url.raw_parts)
    print(" ", dir(request))
    for key, item in dict(
    product=product, release=release, guide=guide, topic=topic
    ).items():
    print(f" {key}={item}")

    found = search_in_database(str(url))
    data = {"rate": found.get("rate")}
    @@ -150,11 +130,10 @@ async def post_rateing(request: web.Request) -> web.StreamResponse:
    # 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
    product = request.match_info.get("product", None)
    release = request.match_info.get("release", None)
    sp = request.match_info.get("sp", None)
    # You can extract the variable with, for example:
    # product = request.match_info.get("product", None)

    url = request.rel_url
    url = str(request.rel_url)

    if request.content_type != "application/json":
    return web.HTTPNotAcceptable(
    @@ -179,7 +158,7 @@ async def post_rateing(request: web.Request) -> web.StreamResponse:
    )

    # All is good, so include it into our database:
    data = dict(product=product, release=release, url=str(url), rate=rate)
    data = dict(product=product, release=release, url=url, rate=rate)
    DATABASE.append(data)

    return web.Response(text=f"Added new rating entry with {data}")
  3. tomschr revised this gist Aug 4, 2021. 1 changed file with 43 additions and 20 deletions.
    63 changes: 43 additions & 20 deletions docrate.py
    Original file line number Diff line number Diff line change
    @@ -54,21 +54,36 @@
    logging.basicConfig(level=logging.DEBUG)
    routes = web.RouteTableDef()

    AVAILABLE_PRODUCTS=("sle-ha", "sle-hpc", "sles", "suma", "slesforsap", ...)
    AVAILABLE_PRODUCTS = ("sle-ha", "sle-hpc", "sles", "suma", "slesforsap", ...)
    DATABASE = [
    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"),
    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) -> dict:
    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:
    @@ -85,7 +100,7 @@ async def handle(request: web.Request) -> web.StreamResponse:
    """
    Example
    """
    name = request.match_info.get('name', "Anonymous")
    name = request.match_info.get("name", "Anonymous")
    text = f"""Hello, {name}"""
    return web.Response(text=text)

    @@ -98,25 +113,27 @@ async def handle(request: web.Request) -> web.StreamResponse:
    # /sle-pos/11-SP3/html/SLEPOS-imgsrv12/index.html
    async def get_rating(request: web.Request) -> web.StreamResponse:
    """
    Async function when a GET with given parameters
    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
    print(" rel_url=", url, dir(url))
    print(" fragment=", url.fragment)
    print(" raw_parts=", url.raw_parts)
    print(" ", dir(request))
    for key, item in dict(product=product, release=release, guide=guide, topic=topic).items():
    for key, item in dict(
    product=product, release=release, guide=guide, topic=topic
    ).items():
    print(f" {key}={item}")

    found = search_in_database(str(url))
    data = {"rate": found.get('rate')}
    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")
    @@ -126,13 +143,17 @@ async def get_rating(request: web.Request) -> web.StreamResponse:
    @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
    product = request.match_info.get("product", None)
    release = request.match_info.get("release", None)
    sp = request.match_info.get("sp", None)

    url = request.rel_url

    if request.content_type != "application/json":
    @@ -144,16 +165,18 @@ async def post_rateing(request: web.Request) -> web.StreamResponse:
    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}")
    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=str(url), rate=rate)
    @@ -169,13 +192,13 @@ async def on_shutdown(app: web.Application) -> None:
    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__':
    if __name__ == "__main__":
    print("DATABASE:", DATABASE)
    main()
  4. tomschr revised this gist Aug 4, 2021. 1 changed file with 11 additions and 63 deletions.
    74 changes: 11 additions & 63 deletions docrate.py
    Original file line number Diff line number Diff line change
    @@ -35,6 +35,7 @@
    * 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
    @@ -64,47 +65,19 @@
    ]


    def get_db_entries(product: str, release: int, sp: Optional[int]):
    """
    Get database entry or None, if no entry is available
    :param product: the abbreviated product string
    :param release: the release (12, 15, ...)
    :param sp: the optional SP or None
    """
    sp_or_empty = "" if sp is None else f"SP{sp}"

    # Filter the database against product and release
    def takewhile():
    yield from (item for item in DATABASE
    if (product==item["product"]) and \
    (release==item["release"])
    )
    # If sp is None, return a generator of all releases
    if sp is None:
    yield from takewhile()

    else:
    # If sp is not None, return a specific release
    sp = int(sp)
    # print(f"Trying to find /{product}/{release}/{sp}...")
    for item in takewhile():
    if sp==item["sp"]:
    yield item

    # If there is no match, an empty generator will be returned


    def search_in_database(url):
    def search_in_database(url) -> dict:
    """
    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)


    # Only for test purposes:
    @routes.get("/")
    @routes.get("/{name}")
    @@ -125,6 +98,7 @@ async def handle(request: web.Request) -> web.StreamResponse:
    # /sle-pos/11-SP3/html/SLEPOS-imgsrv12/index.html
    async def get_rating(request: web.Request) -> web.StreamResponse:
    """
    Async function when a GET with given parameters
    """
    product = request.match_info.get("product")
    release = request.match_info.get("release")
    @@ -136,8 +110,6 @@ async def get_rating(request: web.Request) -> web.StreamResponse:
    print(" rel_url=", url, dir(url))
    print(" fragment=", url.fragment)
    print(" raw_parts=", url.raw_parts)
    # print(" values()=", request.values())
    # print(" headers=", request.headers)
    print(" ", dir(request))
    for key, item in dict(product=product, release=release, guide=guide, topic=topic).items():
    print(f" {key}={item}")
    @@ -150,38 +122,14 @@ async def get_rating(request: web.Request) -> web.StreamResponse:
    # 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}/")
    @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:
    """
    Returns JSON payload from parsed URL
    HINTS:
    * Trailing slash is unimportant
    * Separator between release and SP can be "-" or "/"
    """
    product = request.match_info.get("product")
    release = int(request.match_info.get("release", "0").replace("/", ""))
    sp = request.match_info.get("sp", None)
    sp = None if sp is None else sp.replace("/", "")
    url = request.rel_url

    entries = list(get_db_entries(product, release, sp))
    print("entries:", entries)
    if not entries:
    return web.HTTPNotFound(
    text=f"Rating not found for {product}/{release}/SP{sp}",
    reason=f"No rating found"
    )

    print("request URL:", request.url)
    return web.json_response(entries)


    # @routes.post(r"/{product}/{release:\d+}{sep:[/-]}SP{sp:\d/?}")
    @routes.post(r"/{product}/{release}/html/{guide}/{topic}")
    @routes.post(r"/{product}/{release}/single-html/{guide}/")
    async def p_rateing(request: web.Request) -> web.StreamResponse:
    # 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
    product = request.match_info.get("product", None)
    release = request.match_info.get("release", None)
    sp = request.match_info.get("sp", None)
  5. tomschr created this gist Aug 4, 2021.
    233 changes: 233 additions & 0 deletions docrate.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,233 @@
    #!/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/...)
    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 = [
    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 get_db_entries(product: str, release: int, sp: Optional[int]):
    """
    Get database entry or None, if no entry is available
    :param product: the abbreviated product string
    :param release: the release (12, 15, ...)
    :param sp: the optional SP or None
    """
    sp_or_empty = "" if sp is None else f"SP{sp}"

    # Filter the database against product and release
    def takewhile():
    yield from (item for item in DATABASE
    if (product==item["product"]) and \
    (release==item["release"])
    )
    # If sp is None, return a generator of all releases
    if sp is None:
    yield from takewhile()

    else:
    # If sp is not None, return a specific release
    sp = int(sp)
    # print(f"Trying to find /{product}/{release}/{sp}...")
    for item in takewhile():
    if sp==item["sp"]:
    yield item

    # If there is no match, an empty generator will be returned


    def search_in_database(url):
    """
    Search in the database for given URL
    """
    for item in DATABASE:
    u = item["url"]
    if u == str(url):
    return item
    return dict(rate=None)

    # Only for test purposes:
    @routes.get("/")
    @routes.get("/{name}")
    async def handle(request: web.Request) -> web.StreamResponse:
    """
    Example
    """
    name = request.match_info.get('name', "Anonymous")
    text = f"""Hello, {name}"""
    return web.Response(text=text)


    @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:
    """
    """
    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
    print(" rel_url=", url, dir(url))
    print(" fragment=", url.fragment)
    print(" raw_parts=", url.raw_parts)
    # print(" values()=", request.values())
    # print(" headers=", request.headers)
    print(" ", dir(request))
    for key, item in dict(product=product, release=release, guide=guide, topic=topic).items():
    print(f" {key}={item}")

    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:
    """
    Returns JSON payload from parsed URL
    HINTS:
    * Trailing slash is unimportant
    * Separator between release and SP can be "-" or "/"
    """
    product = request.match_info.get("product")
    release = int(request.match_info.get("release", "0").replace("/", ""))
    sp = request.match_info.get("sp", None)
    sp = None if sp is None else sp.replace("/", "")
    url = request.rel_url

    entries = list(get_db_entries(product, release, sp))
    print("entries:", entries)
    if not entries:
    return web.HTTPNotFound(
    text=f"Rating not found for {product}/{release}/SP{sp}",
    reason=f"No rating found"
    )

    print("request URL:", request.url)
    return web.json_response(entries)


    # @routes.post(r"/{product}/{release:\d+}{sep:[/-]}SP{sp:\d/?}")
    @routes.post(r"/{product}/{release}/html/{guide}/{topic}")
    @routes.post(r"/{product}/{release}/single-html/{guide}/")
    async def p_rateing(request: web.Request) -> web.StreamResponse:
    product = request.match_info.get("product", None)
    release = request.match_info.get("release", None)
    sp = request.match_info.get("sp", None)
    url = 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=str(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()