import os
from collections import defaultdict
from urllib.parse import urlencode, urljoin, urlparse, urlunparse
import ombott
from py4web import request
def url_of(
controller,
*path_args,
filter_by_method=None,
filter_by_appname=None,
add_query=None,
add_scheme=None,
add_anchor=None,
**path_kwargs,
):
"""
Generates a URL for the given controller.
Controllers can be passed by their function object or by name.
Accepts extra positional and keyword arguments. These are used
to fill url patterns like: <some_id:int>
The number of positional and keyword arguments summed must match the
number of patterns in the URL. Keyword arguments take precedence,
positional arguments fill whatever patterns are left. That means a if you have a
controller named foobar with path /api/object/<some_id:int>/value/<field:str>
and call `url_of('foobar', 'baz', some_id=5)` the returned URL will be:
/api/object/5/value/baz
#### Arguments:
- `filter_by_method` - only get urls which have a handler registered
capable of handling this HTTP request method
- `filter_by_appname` - get urls for a different app_name
- `add_query` - dictionary of query params to be added to url
- `add_scheme` - bool or str, bool toggles the scheme (https:// etc.) found in the
current request on or off, str overwrites the scheme.
None by default, which outputs a relative url
#### Examples:
- `url_of('index')` -> `{app_name?}/index`
- `url_of('api_object_thing', id=5)` -> `{app_name?}/api/object/5/thing`
- `url_of('index', add_scheme=True)` -> `https://{domain}/{app_name?}/index`
app_name = filter_by_appname else:
has_scriptname = len(request.script_name) > 1 app_name = request.fullpath.split("/")[1 + has_scriptname] if isinstance(controller, str): # if controller_name contains a ".", treat it as the path separator controller_path = controller.split(".") else:
controller_path = [*controller.__module__.split("."), controller.__name__] condition_txt = "this name" routes = ombott.app.routes.values() handler_routes = defaultdict(list) for route in routes:
# needs to be a route of this app # route.pattern is a url like someapp/part1/part2 if not route.pattern.startswith(app_name): condition_txt = f" app {app_name}" continue
# the number of params needs to match if len(route.params) != len(path_args) + len(path_kwargs): condition_txt = f"{len(path_args) + len(path_kwargs)} path args" continue
# all path_kwargs need to be known params of the route if any(kwarg not in route.params for kwarg in path_kwargs.keys()): condition_txt = f"these path kwargs {tuple(path_kwargs.keys())}" continue
if filter_by_method:
meth = route.methods.get(filter_by_method, None)
if meth is None:
condition_txt = f"HTTP method {filter_by_method}" continue
methods = [meth]
else:
methods = route.methods.values()
for handler in methods:
# one of the methods needs to match the controller name # method.handler_fullname is the function name import path # dot-separated: apps.<appname>.<controllers_file>.<controllerfuncname>
handler_path = handler.handler_fullname.split(".") if handler_path[-len(controller_path) :] != controller_path: condition_txt = f"controller path '{controller_path}'" continue
handler_routes[handler.handler_fullname].append(route) if len(handler_routes) == 0: raise ValueError(f"No controller '{controller}' with {condition_txt} found.") if len(handler_routes) > 1: optstr = "\n - ".join(f"{k}: {rms[0].route.rule}" for k, rms in handler_routes.items()) raise ValueError(f"More than 1 url route found for specified controller and arguments:\n{optstr}") route = next(iter(handler_routes.values()))[0] pos_args = list(reversed(path_args)) params = {p: path_kwargs.get(p, None) or pos_args.pop() for p in route.params} url = route.url(**params) # if domain-mapped, remove prepended appname if request.environ.get("HTTP_X_PY4WEB_APPNAME"): # if its also prefixed, have to remove prefix before appname is removed # since bottle routes are registered including the prefix prefix = os.environ.get("PY4WEB_URL_PREFIX", "").strip("/") split_url = url.split("/") if prefix:
prefix_parts = len(prefix.split("/")) split_url = [prefix, *split_url[prefix_parts + 1 :]] else:
split_url = split_url[1:] url = "/".join(split_url) if add_query:
url = urljoin(url, "?" + urlencode(add_query)) if add_scheme is False:
scheme = ""
elif isinstance(add_scheme, str): scheme = add_scheme
else:
request_url = request.environ.get("HTTP_ORIGIN") or request.url scheme = urlparse(request_url).scheme if add_scheme is not None:
url = url._replace(scheme=scheme) if add_anchor:
url = url._replace(fragment=urlencode(add_anchor))