Skip to content

Instantly share code, notes, and snippets.

@awbacker
Last active May 17, 2016 08:46
Show Gist options
  • Save awbacker/2a868e9ab56a821e6a3364927798e582 to your computer and use it in GitHub Desktop.
Save awbacker/2a868e9ab56a821e6a3364927798e582 to your computer and use it in GitHub Desktop.
URL Helper for Django / Django Rest Framework
# -*- 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