Last active
May 17, 2016 08:46
-
-
Save awbacker/2a868e9ab56a821e6a3364927798e582 to your computer and use it in GitHub Desktop.
URL Helper for Django / Django Rest Framework
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
# -*- coding: utf-8 -* | |
# | |
# URL_EX : url helper to make dealing with patterns in urls easier, and prevent common issues | |
# - calls .as_view() on DRF APIView instances automatically | |
# - appends a leading ^ | |
# - appends a trailing $ the the call isn't an include() | |
# - add more shortcuts and patterns as required | |
# | |
# Examples: | |
# urlpatterns = [ | |
# url_ex('accounts/:id/', GetAccountView), => r'accounts/(?<id>\d+)/$' | |
# url_ex('accounts/:id/', ..., case_sensitive=False) => r'(?i)accounts/(?<id>\d+)/$' | |
# ] | |
# | |
# Patterns are one of ":shortcut" or ":name:var_type" | |
# - if the variable does not have a :int (or other type at the end) we will split on "_" and use the end part | |
# - as the shortcut to use. so "new_id" == "name=new_id, shortcut=id", where id is an int. | |
# | |
# - "user/:pk/" gets a variable named 'pk', using the 'pk' shortcut to define the data type regex | |
# - "user/:oldname:int/" integer variable named "abc" | |
# - "user/:my_id/ variable named 'my_id', using the 'id' shortcut for the data type | |
# - "user/:my_id:uuid/ variable named 'my_id', but of type 'uuid' | |
# | |
from __future__ import print_function, absolute_import, unicode_literals | |
import re | |
__all__ = [ | |
'url_ex' | |
] | |
VAR_TYPES = dict( | |
int=r'\d+', | |
decimal=r'\d+(\.\d+)?', | |
uuid=r'[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[1345][0-9A-Fa-f]{3}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}', | |
slug=r'[-\w]+', | |
hex='[0-9A-Fa-f]+', | |
) | |
VAR_SHORTCUTS = dict( | |
id=VAR_TYPES['int'], | |
pk=VAR_TYPES['int'], | |
fk=VAR_TYPES['int'], | |
slug=VAR_TYPES['slug'], | |
uuid=VAR_TYPES['uuid'], | |
) | |
VARNAME = r'([a-zA-Z]\w*)' | |
RE_EXPLICIT = r'(:{var}:([a-z]+))'.format(var=VARNAME) | |
RE_SHORTCUT = r'(:{var})'.format(var=VARNAME) | |
def url_ex(url, view, kwargs=None, name=None, prefix='', case_sensitive=True): | |
from django.conf.urls import url as djang_url | |
# if the view has a function called 'as_view' which is callable, then we are probably | |
# dealing with a class based view and need to call as_view first. This should help remove | |
# some cryptic errors when this is not done by the user | |
if callable(getattr(view, 'as_view', None)): | |
view = view.as_view() | |
# Don't add a $ when including sub-views via include('my.app.urls') => (module,app,namespace) | |
add_eol = not (isinstance(view, tuple) and len(view) == 3) | |
url = '^{case}{url}{eol}'.format( | |
url=url.lstrip(' ^\n').rstrip(' $\n'), | |
eol='$' if add_eol else '', | |
case="(?i)" if not case_sensitive else "" | |
) | |
return djang_url(_patch_url(url), view, kwargs=kwargs, name=name, prefix=prefix) | |
def _patch_url(url): | |
new_url = _patch_typed_variables(url, original_url=url) | |
new_url = _patch_shortcuts(new_url, original_url=url) | |
return new_url | |
def _url_var(var_name, type_regex): | |
""" | |
Creates the regex for a django URL variable | |
""" | |
return '(?P<{name}>{regex})'.format(name=var_name, regex=type_regex) | |
def _patch_shortcuts(url, original_url): | |
""" | |
Patch all short variables (the ones without an explicit data type) | |
""" | |
found_shortcuts = re.findall(RE_SHORTCUT, url) | |
var_names = [var for _, var in found_shortcuts] | |
if len(var_names) != len(set(var_names)): | |
raise ValueError("Duplicate variable found in url %s" % original_url) | |
for full_match, var_name in found_shortcuts: | |
url = _patch_single_shortcut(url, full_match, var_name, original_url) | |
return url | |
def _patch_single_shortcut(url, full_match, var_name, original_url): | |
# check for full match first (more specific) before checking for ends with, in case | |
# we have two shortcuts like ('id=\d+' and my_id=r'AF\d+'). | |
if var_name in VAR_SHORTCUTS: | |
return url.replace(full_match, _url_var(var_name, VAR_SHORTCUTS[var_name])) | |
for key, regex in VAR_SHORTCUTS.iteritems(): | |
if var_name.endswith('_%s' % key): | |
return url.replace(full_match, _url_var(var_name, regex)) | |
raise ValueError("Name %s is not a valid shortcut (url=%s)" % (var_name, original_url)) | |
def _patch_typed_variables(url, original_url): | |
""" | |
Replaces all occurances of ":var:type" found in the URL | |
:type url: str | |
""" | |
typed_vars = re.findall(RE_EXPLICIT, url) | |
var_names = [n for _, n, _ in typed_vars] | |
if len(var_names) != len(set(var_names)): | |
raise ValueError("Duplicate typed variable found in url %s" % original_url) | |
for full_match, var_name, typ in typed_vars: | |
if not typ in VAR_TYPES: | |
raise ValueError("Type pattern '%s' not found (url=%s)" % (typ, original_url)) | |
url = url.replace(full_match, _url_var(var_name, VAR_TYPES[typ])) | |
return url |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment