Skip to content

Commit

Permalink
🧭 handling of aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
Joshix-1 committed Jan 18, 2024
1 parent ce2baa9 commit e7a4d90
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 56 deletions.
6 changes: 3 additions & 3 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions an_website/backdoor/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def main() -> int | str: # noqa: C901
"""\
Accepted arguments:
--dev use a separate config for a local developing instance
--dev use a separate config for a local dev instance
--lisp enable Lots of Irritating Superfluous Parentheses
--new-proxy don't use the saved proxy
--new-session start a new session with saved URL and key
Expand Down Expand Up @@ -619,8 +619,7 @@ def send_to_remote(code: str, *, mode: str) -> Any:
" pydoc.Helper(io.StringIO(), helper_output)(*args, **kwargs)\n"
" return 'PagerTuple', helper_output.getvalue()\n"
f" __str__ = __repr__ = lambda _:{repr(help)!r}\n" # noqa: E131
"help = _HelpHelper_92005ecf3788faea8346a7919fba0232188561ab()\n"
"del _HelpHelper_92005ecf3788faea8346a7919fba0232188561ab",
"help = _HelpHelper_92005ecf3788faea8346a7919fba0232188561ab()\n",
# fmt: on
mode="exec",
)
Expand Down
8 changes: 5 additions & 3 deletions an_website/endpoints/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from typing import ClassVar, cast

import orjson as json
from tornado.web import RedirectHandler

from .. import ORJSON_OPTIONS
from ..utils.request_handler import APIRequestHandler, HTMLRequestHandler
Expand All @@ -31,12 +30,15 @@ def get_module_info() -> ModuleInfo:
handlers=(
("/endpunkte", Endpoints),
("/api/endpunkte", EndpointsAPI),
("/api/endpoints/*", RedirectHandler, {"url": "/api/endpunkte"}),
),
name="API-Endpunkte",
description="Alle API-Endpunkte unserer Webseite",
path="/endpunkte",
aliases=("/endpoints",),
aliases={
"/endpoints": "/endpunkte",
"/api": "/api/endpunkte",
"/api/endpoints": "/api/endpunkte",
},
keywords=("API", "Endpoints", "Endpunkte"),
)

Expand Down
71 changes: 34 additions & 37 deletions an_website/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,20 @@
from asyncio import AbstractEventLoop
from asyncio.runners import _cancel_all_tasks # type: ignore[attr-defined]
from base64 import b64encode
from collections.abc import Callable, Coroutine, Iterable, MutableSequence
from collections.abc import (
Callable,
Coroutine,
Iterable,
Mapping,
MutableSequence,
)
from configparser import ConfigParser
from functools import partial
from hashlib import sha256
from multiprocessing.process import _children # type: ignore[attr-defined]
from pathlib import Path
from socket import socket
from typing import Any, Final, Literal, TypeAlias, TypedDict, cast
from typing import Any, Final, Literal, TypeAlias, TypedDict, TypeGuard, cast
from warnings import catch_warnings, simplefilter
from zoneinfo import ZoneInfo

Expand Down Expand Up @@ -291,7 +297,6 @@ def get_all_handlers(module_infos: Iterable[ModuleInfo]) -> list[Handler]:
for module_info in module_infos:
for handler in module_info.handlers:
handler = list(handler) # pylint: disable=redefined-loop-name
handler[0] = "(?i)" + handler[0]
# if the handler is a request handler from us
# and not a built-in like StaticFileHandler & RedirectHandler
if issubclass(handler[1], BaseRequestHandler):
Expand All @@ -308,24 +313,9 @@ def get_all_handlers(module_infos: Iterable[ModuleInfo]) -> list[Handler]:
else:
handler[2]["module_info"] = module_info
handlers.append(tuple(handler))
if module_info.path is not None:
for alias in module_info.aliases:
handlers.append(
(
# (?i) -> ignore case
# (.*) -> add group that matches anything
"(?i)" + alias + "(/.*|)",
RedirectHandler,
# {0} -> the part after the alias (/.*) or ""
{"url": module_info.path + "{0}"},
)
)

# redirect handler, to make finding APIs easier
handlers.append((r"(?i)/(.+)/api/*", RedirectHandler, {"url": "/api/{0}"}))

# redirect from /api to /api/endpunkte (not with alias, because it fails)
handlers.append((r"(?i)/api/*", RedirectHandler, {"url": "/api/endpunkte"}))
handlers.append((r"/(.+)/api/*", RedirectHandler, {"url": "/api/{0}"}))

handlers.append(
(
Expand Down Expand Up @@ -354,29 +344,36 @@ def get_normed_paths_from_module_infos(
) -> dict[str, str]:
"""Get all paths from the module infos."""

def norm_paths(paths: Stream[str | None] | Stream[str]) -> Stream[str]:
return (
paths.filter()
.filter(lambda path: path.startswith("/"))
.map(str.strip, "/")
.filter(lambda p: len(p) > 1)
.map(str.lower)
)
def tuple_has_no_none(
value: tuple[str | None, str | None]
) -> TypeGuard[tuple[str, str]]:
return None not in value

def info_to_paths(info: ModuleInfo) -> Iterable[tuple[str, str]]:
def info_to_paths(info: ModuleInfo) -> Stream[tuple[str, str]]:
return (
norm_paths(Stream(info.aliases).chain([info.path])).map(
lambda path: (path, cast(str, info.path)) # type: ignore[redundant-cast]
Stream(((info.path, info.path),))
.chain(
info.aliases.items()
if isinstance(info.aliases, Mapping)
else ((alias, info.path) for alias in info.aliases)
)
if info.path
else Stream(())
).chain(
norm_paths(
Stream(info.sub_pages).map(lambda sub_info: sub_info.path)
).map(lambda path: (path, path))
.chain(
Stream(info.sub_pages)
.map(lambda sub_info: sub_info.path)
.filter()
.map(lambda path: (path, path))
)
.filter(tuple_has_no_none)
)

return Stream(module_infos).flat_map(info_to_paths).collect(dict)
return (
Stream(module_infos)
.flat_map(info_to_paths)
.filter(lambda p: p[0].startswith("/"))
.map(lambda p: (p[0].strip("/").lower(), p[1]))
.filter(lambda p: p[0])
.collect(dict)
)


def make_app(config: ConfigParser) -> str | Application:
Expand Down
27 changes: 25 additions & 2 deletions an_website/utils/request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,33 @@ async def prepare(self) -> None:

this_path_normalized = unquote(new_path).strip("/").lower()

if len(this_path_normalized) == 1:
paths: dict[str, str] = self.settings.get("NORMED_PATHS") or {}

if p := paths.get(this_path_normalized):
return self.redirect(self.fix_url(new_path=p), False)

if len(this_path_normalized) <= 1:
return self.redirect(self.fix_url(new_path="/"))

paths: dict[str, str] = self.settings.get("NORMED_PATHS") or {}
prefixes = tuple(
(p, repl)
for p, repl in paths.items()
if this_path_normalized.startswith(f"{p}/")
)
if len(prefixes) == 1:
((prefix, replacement),) = prefixes
return self.redirect(
self.fix_url(
new_path=f"{replacement.strip("/")}"
f"{this_path_normalized.removeprefix(prefix)}"
),
False,
)
if prefixes:
LOGGER.error(
"Too many prefixes %r for path %s", prefixes, self.request.path
)

matches = get_close_matches(this_path_normalized, paths, count=1)
if matches:
return self.redirect(
Expand Down
2 changes: 1 addition & 1 deletion an_website/utils/static_file_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def create_file_hashes_dict() -> dict[str, str]:

def get_handlers() -> list[Handler]:
"""Return a list of handlers for static files."""
# pylint: disable=import-outside-toplevel
# pylint: disable=import-outside-toplevel, cyclic-import
from .static_file_from_traversable import TraversableStaticFileHandler

handlers: list[Handler] = [
Expand Down
11 changes: 9 additions & 2 deletions an_website/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@
import sys
import time
from base64 import b85encode
from collections.abc import Awaitable, Callable, Generator, Iterable, Set
from collections.abc import (
Awaitable,
Callable,
Generator,
Iterable,
Mapping,
Set,
)
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import IntFlag
Expand Down Expand Up @@ -821,7 +828,7 @@ class ModuleInfo(PageInfo):

handlers: tuple[Handler, ...] = field(default_factory=tuple[Handler, ...])
sub_pages: tuple[PageInfo, ...] = field(default_factory=tuple)
aliases: tuple[str, ...] = field(default_factory=tuple)
aliases: tuple[str, ...] | Mapping[str, str] = field(default_factory=tuple)

def get_keywords_as_str(self, path: str) -> str:
"""Get the keywords as comma-seperated string."""
Expand Down
2 changes: 1 addition & 1 deletion pip-constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ redis==5.0.1
regex==2023.12.25
setproctitle==1.3.3
soupsieve==2.5
typed-stream==0.42.0
typed-stream==0.69.0
tzdata==2023.4
ultradict==0.0.6
uvloop==0.19.0
Expand Down
2 changes: 1 addition & 1 deletion pip-dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ redis[hiredis]==5.0.1; python_version >= '3.7'
regex==2023.12.25; python_version >= '3.7'
setproctitle==1.3.3; python_version >= '3.7'
soupsieve==2.5; python_version >= '3.8'
typed-stream==0.42.0; python_version >= '3.10'
typed-stream==0.69.0; python_version >= '3.10'
tzdata==2023.4; python_version >= '2'
ultradict==0.0.6; python_version >= '3.8'
uvloop==0.19.0; python_full_version >= '3.8.0'
Expand Down
2 changes: 1 addition & 1 deletion pip-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ setproctitle==1.3.3; python_version >= '3.7'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
soupsieve==2.5; python_version >= '3.8'
tornado==6.4; python_version >= '3.8'
typed-stream==0.42.0; python_version >= '3.10'
typed-stream==0.69.0; python_version >= '3.10'
typing-extensions==4.9.0; python_version < '3.12'
tzdata==2023.4; python_version >= '2'
ultradict==0.0.6; python_version >= '3.8'
Expand Down
17 changes: 15 additions & 2 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@

import asyncio
import pathlib
import typing
from collections.abc import Mapping

import regex
from tornado.simple_httpclient import SimpleAsyncHTTPClient
from tornado.web import Application

from an_website import main, patches
Expand Down Expand Up @@ -63,8 +66,18 @@ async def test_parsing_module_infos(
for alias in module_info.aliases:
assert alias.startswith("/")
assert not alias.endswith("/")
if module_info.path != "/chat" and alias.isascii():
await assert_valid_redirect(fetch, alias, module_info.path)
if module_info.path != "/chat":
should_path: str = (
module_info.aliases[alias]
if isinstance(module_info.aliases, Mapping)
else module_info.path
)
kwargs: dict[str, typing.Any] = (
{}
if alias.isascii()
else {"httpclient": SimpleAsyncHTTPClient()}
)
await assert_valid_redirect(fetch, alias, should_path, **kwargs)

if module_info.path != "/api/update":
# check if at least one handler matches the path
Expand Down

0 comments on commit e7a4d90

Please sign in to comment.