From fb50596d22b101031b7ff6d84c818310c8862456 Mon Sep 17 00:00:00 2001 From: Luke Swanson Date: Fri, 9 Jan 2026 13:17:36 -0600 Subject: [PATCH] Add custom urljoin for more-nicely working with paths to WSGI apps. --- src/ogd/apis/utils/APIUtils.py | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/ogd/apis/utils/APIUtils.py b/src/ogd/apis/utils/APIUtils.py index 1177f2b..2e716aa 100644 --- a/src/ogd/apis/utils/APIUtils.py +++ b/src/ogd/apis/utils/APIUtils.py @@ -11,6 +11,7 @@ from json.decoder import JSONDecodeError from logging import Logger from typing import Any, List, Optional +from urllib import parse # import OGD libraries from ogd.common.storage.interfaces.Interface import Interface @@ -42,6 +43,46 @@ def parse_list(list_str:str, logger:Optional[Logger]=None) -> Optional[List[Any] ret_val = None return ret_val +def urljoin(base:str, url:str, ignore_base_file:bool=False, allow_fragments:bool=True): + """Custom variation of the `urllib.parse.urljoin` function provided by Python. + + By default, this version allows filenames in the base path to remain in the joined path. + + This is useful for working with Flask apps, particularly on Apache. + Specifically, unless you alias things in Apache, you'll have `app.py` or `app.wsgi` in the URL. + For a Flask API, then, you'll likely be joining a base URL like `"https://host.of.app/path/to/app.wsgi"` with an endpoint, call it `"endpoint"`. + Under `urllib.parse.urljoin`, you'll get `"https://host.of.app/path/to/endpoint"`. + With _this_ function, setting `ignore_base_file=False`, you'll get `"http://host.of.app/path/to/app.wsgi/endpoint"` as desired. + + When `ignore_base_file=True`, this function directly falls back to use `urllib.parse.urljoin`. + + :param base: The base URL, onto which the `url` parameter is joined. + :type base: str + :param url: The URL to be joined onto the given base URL. + :type url: str + :param ignore_base_file: Whether to ignore filenames in the base URL. + When True, such filenames are removed from the joined URL. + For example, joining `https://host.of.app/path/to/app.wsgi` with `endpoint` would yield `https://host.of.app/path/to/endpoint`. + When False, such filenames are included in the joined URL. + :type ignore_base_file: bool + :param allow_fragments: Whether to allow fragment parts in the URLs. + This is only used when `ignore_base_file=True`, in which case this function falls back on `urllib.parse.urljoin` and `allow_fragments` is passed to that function call. + :type ignore_base_file: bool + """ + if ignore_base_file: + return parse.urljoin(base=base, url=url, allow_fragments=allow_fragments) + else: + # Make sure we have a scheme + if not (base.startswith("http://") or base.startswith("https://")): + base = f"https://{base}" + # If base ends with a /, remove it so we don't double-up when joining + if base.endswith("/"): + base = base[:-1] + # If url starts with a /, remove it so we don't double-up when joining + if url.startswith("/"): + url = url[1:] + return f"{base}/{url}" + # def gen_interface(game_id, core_config:ConfigSchema, logger:Optional[Logger]=None) -> Optional[Interface]: # """Utility to set up an Interface object for use by the API, given a game_id.