diff --git a/dr_botzo/dr_botzo/urls.py b/dr_botzo/dr_botzo/urls.py index 6196d80..094df74 100644 --- a/dr_botzo/dr_botzo/urls.py +++ b/dr_botzo/dr_botzo/urls.py @@ -10,6 +10,7 @@ urlpatterns = patterns('', url(r'^$', 'dr_botzo.views.home', name='home'), url(r'^dispatch/', include('dispatch.urls')), + url(r'^facts/', include('facts.urls')), url(r'^markov/', include('markov.urls')), url(r'^races/', include('races.urls')), diff --git a/dr_botzo/facts/soap.py b/dr_botzo/facts/soap.py new file mode 100644 index 0000000..322220f --- /dev/null +++ b/dr_botzo/facts/soap.py @@ -0,0 +1,195 @@ +"""Do dispatcher stuff over SOAP. POC code.""" + +from __future__ import unicode_literals + +import logging + +from pysimplesoap.server import SoapDispatcher +from django.conf import settings +from django.contrib.sites.models import Site +from django.core.urlresolvers import reverse_lazy +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt + +from facts.models import Fact, FactCategory + + +log = logging.getLogger(__name__) + + +class ComplexType(object): + + _type_info = None + + def __init__(self, data=None): + """Bind data to this instance, if provided. + + :param data: data to bind, if desired + :type data: _type_info + """ + + self.data = None + if data: + ComplexType._checker(data, self._type_info) + # exception would have been raised if there was a problem + self.data = data + + @staticmethod + def _checker(data, type_info, label=None): + label = '\'{0:s}\''.format(label) if label else 'data' + + if isinstance(type_info, dict): + if not isinstance(data, dict): + raise Exception("{0:s} is supposed to be a dict but is {1:s}".format(label, type(data))) + + for key in type_info.keys(): + try: + sub_data = data[key] + except KeyError: + raise Exception("key '{0:s}' not in data {1:s}".format(key, data)) + + ComplexType._checker(sub_data, type_info[key], label=key) + elif isinstance(type_info, list): + if not isinstance(data, list): + raise Exception("{0:s} is supposed to be a list but is {1:s}".format(label, type(data))) + + for item in data: + ComplexType._checker(item, type_info[0], label='item in list {0:s}'.format(label)) + else: + if type_info != type(data): + raise Exception("{0:s} is supposed to be {1:s} but is {2:s}".format(label, type_info, type(data))) + + +class WsFact(ComplexType): + _type_info = { + 'fact': { + 'key': unicode, + 'fact': unicode, + 'nickmask': unicode, + } + } + + +class WsFactList(ComplexType): + _type_info = { + 'facts': [WsFact._type_info] + } + + +def get_fact_by_key(key, regex=None): + """Get a fact of the matching key and regex. + + If there are multiple matching facts, you get a random one. + + :param key: fact category to search for + :type key: unicode + :param regex: regex to filter results by (optional) + :type regex: unicode + """ + + fact = Fact.objects.random_fact(key, regex) + + ret = {'key': fact.category.name, 'fact': fact.fact, 'nickmask': fact.nickmask} + return WsFact({'fact': ret}).data + + +def get_facts_by_key(key, regex=None): + """Get all facts of the matching key and regex.""" + + try: + fact_category = FactCategory.objects.get(name=key) + except FactCategory.DoesNotExist: + return None + + facts = Fact.objects.filter(category=fact_category) + ret_facts = [] + for fact in facts: + ret_fact = {'key': fact.category.name, 'fact': fact.fact, 'nickmask': fact.nickmask} + ret_facts.append({'fact': ret_fact}) + + return WsFactList({'facts': ret_facts}).data + + +class LazyAbsoluteUrl(object): + + """Use (abuse?) duck typing to wrap reverse_lazy in a manner that lets us + append domain stuff but still be lazy, and appear as a string. + + We need to do this lazily because SoapRequest objects are probably created at the module + level, which means they'll probably first get hit during urls.py scanning, and we can't + reference reverse() then (as urls haven't finished importing). Since we need a domain and + stuff, we need to do that lazily too, and can't simply use reverse_lazy(). Hence this + class that tries to look like a string + """ + + def __init__(self, url_name): + """Set up for generating an absolute lazy URL, note what name to reverse_lazy() + eventually. + + :param url_name: the name (as registered from urls.py) to look up with reverse_lazy + :type url_name: unicode + """ + + self.url_name = url_name + + def __add__(self, other): + """SoapDispatcher tries to add self.action (this) to a string, so we need + to support that. + + :param other: string to concatenate + :type other: str + :returns: concatenated string + :rtype: str + """ + + return '{0:s}{1:s}'.format(self, other) + + def __str__(self): + """Lazily generate a reverse absolute URL, via Site + url_name lookup. + + :returns: absolute URL + :rtype: str + """ + + return 'http://{0:s}{1:s}'.format(Site.objects.get_current().domain, + reverse_lazy(self.url_name)) + + def replace(self, *args, **kwargs): + """We're kind of tricking things to think we're a string in SoapDispatcher, + so we need to look like a string. + """ + + return '{0:s}'.format(self).replace(*args, **kwargs) + + +# create the SOAP dispatcher +# note - setting debug lets exception details include the traceback +lazy_soap_url = LazyAbsoluteUrl('facts_soap_dispatcher') +soap = SoapDispatcher( + name='facts', + location=lazy_soap_url, + action=lazy_soap_url, + namespace=lazy_soap_url, prefix='drb', + documentation="dr.botzo Facts API", + debug=settings.DEBUG, +) + + +# register functions with the SOAP dispatcher +soap.register_function('getFactByKey', get_fact_by_key, args={'key': unicode, 'regex': unicode}, + returns=WsFact._type_info) +soap.register_function('getFactsByKey', get_facts_by_key, args={'key': unicode, 'regex': unicode}, + returns=WsFactList._type_info) + + +# django URL handler, point urls.py at this +@csrf_exempt +def dispatcher_handler(request): + if request.method == "POST": + response = HttpResponse(content_type="application/xml") + response.write(soap.dispatch(request.body)) + else: + response = HttpResponse(content_type="application/xml") + response.write(soap.wsdl()) + response['Content-length'] = str(len(response.content)) + return response diff --git a/dr_botzo/facts/urls.py b/dr_botzo/facts/urls.py new file mode 100644 index 0000000..4b98c25 --- /dev/null +++ b/dr_botzo/facts/urls.py @@ -0,0 +1,8 @@ +"""URL patterns for the facts API.""" + +from django.conf.urls import patterns, url + + +urlpatterns = patterns('', + url(r'^soap/', 'facts.soap.dispatcher_handler', name='facts_soap_dispatcher'), +)