├── .gitignore
├── AUTHORS
├── Changelog
├── INSTALL
├── MANIFEST.in
├── Makefile
├── README
├── README.rst
├── TODO
├── contrib
└── doc2ghpages
├── docs
├── Makefile
├── _ext
│ ├── applyxrefs.py
│ └── literals_to_xrefs.py
├── _theme
│ └── nature
│ │ ├── static
│ │ ├── nature.css_t
│ │ └── pygments.css
│ │ └── theme.conf
├── changelog.rst
├── conf.py
├── index.rst
├── introduction.rst
└── reference
│ ├── durian.event.rst
│ ├── durian.forms.rst
│ ├── durian.match.able.rst
│ ├── durian.match.rst
│ ├── durian.match.strategy.rst
│ ├── durian.models.rst
│ ├── durian.tasks.rst
│ ├── durian.views.rst
│ └── index.rst
├── durian
├── __init__.py
├── admin.py
├── event.py
├── forms.py
├── match
│ ├── __init__.py
│ ├── able.py
│ └── strategy.py
├── models.py
├── registry.py
├── tasks.py
├── templates
│ └── durian
│ │ ├── base.html
│ │ ├── base_site.html
│ │ ├── create_hook.html
│ │ └── select_hook.html
├── tests
│ ├── __init__.py
│ ├── test_hooks.py
│ └── test_match.py
├── urls.py
└── views.py
├── examples
├── durianproject
│ ├── __init__.py
│ ├── durianapp
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── manage.py
│ ├── settings.py
│ └── urls.py
└── hook.py
├── setup.cfg
├── setup.py
└── testproj
├── __init__.py
├── manage.py
├── settings.py
└── urls.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.pyc
3 | *~
4 | *.sqlite
5 | *.sqlite-journal
6 | settings_local.py
7 | local_settings.py
8 | .*.sw[po]
9 | dist/
10 | *.egg-info
11 | doc/__build/*
12 | build/
13 | locale/
14 | pip-log.txt
15 | devdatabase.db
16 | .directory
17 | bundle_version.gen
18 | celeryd.log
19 | celeryd.pid
20 | docs/.build
21 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | Ask Solem
2 |
3 | Inspired by John Boxall's django-webhooks
4 | http://github.com/johnboxall/django_webhooks/tree/master
5 |
--------------------------------------------------------------------------------
/Changelog:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ask/durian/bd7a773ce3470884fd74e97f0bba4b351545df56/Changelog
--------------------------------------------------------------------------------
/INSTALL:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | You can install ``durian`` either via the Python Package Index (PyPI)
5 | or from source.
6 |
7 | To install using ``pip``,::
8 |
9 | $ pip install durian
10 |
11 |
12 | To install using ``easy_install``,::
13 |
14 | $ easy_install durian
15 |
16 |
17 | If you have downloaded a source tarball you can install it
18 | by doing the following,::
19 |
20 | $ python setup.py build
21 | # python setup.py install # as root
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS
2 | include Changelog
3 | include INSTALL
4 | include MANIFEST.in
5 | include README
6 | include TODO
7 | recursive-include docs *
8 | recursive-include durian *
9 | recursive-include testproj *
10 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PEP8=pep8
2 |
3 | pep8:
4 | (find . -name "*.py" | xargs pep8 | perl -nle'\
5 | print; $$a=1 if $$_}{exit($$a)')
6 |
7 | cycomplex:
8 | find celery -type f -name "*.py" | xargs pygenie.py complexity
9 |
10 | ghdocs:
11 | contrib/doc2ghpages
12 |
13 | autodoc:
14 | contrib/doc4allmods celery
15 |
16 | bump:
17 | contrib/bump -c celery
18 |
19 | coverage2:
20 | [ -d testproj/temp ] || mkdir -p testproj/temp
21 | (cd testproj; python manage.py test --figleaf)
22 |
23 | coverage:
24 | [ -d testproj/temp ] || mkdir -p testproj/temp
25 | (cd testproj; python manage.py test --coverage)
26 |
27 | test:
28 | (cd testproj; python manage.py test)
29 |
30 | testverbose:
31 | (cd testproj; python manage.py test --verbosity=2)
32 |
33 | releaseok: pep8 autodoc test
34 |
35 | removepyc:
36 | find . -name "*.pyc" | xargs rm
37 |
38 | release: releaseok ghdocs removepyc
39 |
40 |
--------------------------------------------------------------------------------
/README:
--------------------------------------------------------------------------------
1 | README.rst
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ============================================================================
2 | durian - Web Hooks for Django
3 | ============================================================================
4 |
5 | :Version: 0.1.0
6 |
7 | .. image:: http://cloud.github.com/downloads/ask/durian/webhooks-logo.png
8 |
9 | Introduction
10 | ============
11 |
12 | We want the web sites we create to communicate with other sites. To enable
13 | this we give the clients an URL they can connect to. This is fine for most
14 | requests, but let's take a look at RSS.
15 |
16 | RSS publishes your articles for others to subscribe to. Whenever you have a
17 | new article to publish you add it to the RSS document available at an URL
18 | like::
19 |
20 | http://example.com/articles.rss
21 |
22 | The client connects to this URL, say, every 20 minutes to check if there's
23 | something new. And if there is something new, it has to re-download the entire
24 | content, even if it already has some of the articles from before.
25 | We call this communication method `pulling`_.
26 |
27 | This is where web hooks (or HTTP callbacks) comes in, instead of giving the
28 | clients an URL they can connect to, the clients *give you an URL* you connect
29 | to every time there is something to update.
30 |
31 | By `pushing`_ instead of pulling the updates, both you
32 | and your clients saves bandwidth, sometimes by a lot.
33 |
34 | .. image:: http://cloud.github.com/downloads/ask/durian/webhook-callback2.png
35 |
36 | You can read more about web hooks at the `Web Hooks Blog`_.
37 | These slides by Jeff Lindsay is a good introduction to the subject:
38 | `Using Web Hooks`_.
39 |
40 | .. _`Web Hooks Blog`: http://blog.webhooks.org
41 | .. _`Using Web Hooks`:
42 | http://www.slideshare.net/progrium/using-web-hooks
43 | .. _`pushing`: http://en.wikipedia.org/wiki/Push_technology
44 | .. _`pulling`: http://en.wikipedia.org/wiki/Pull_technology
45 |
46 | **NOTE** This software is just in the planning stage and is going to
47 | change drastically. You can follow what is happening here, and is welcome to
48 | help out making it happen, but you should probably not use it for anything
49 | until it has reached an alpha version.
50 |
51 |
52 | Examples
53 | ========
54 |
55 | Creating an event with a model and a signal
56 | -------------------------------------------
57 |
58 | In this example we'll be creating a ModelHook.
59 |
60 | A ModelHook is a hook which takes a Django model and signal.
61 | So whenever that signal is fired, the hook is also triggered.
62 |
63 | You can specify which of the model fields you want to pass on to the listeners
64 | via the ``provides_args`` attribute.
65 |
66 |
67 | First let's create a simple model of a person storing the persons
68 | name, address and a secret field we don't want to pass on to listeners:
69 |
70 | >>> from django.db import models
71 | >>> from django.utils.translation import ugettext_lazy as _
72 |
73 | >>> class Person(models.Model):
74 | ... name = models.CharField(_(u"name"), blank=False, max_length=200)
75 | ... address = models.CharField(_(u"address"), max_length=200)
76 | ... secret = models.CharField(_(u"secret"), max_length=200)
77 |
78 |
79 | Now to the hook itself. We subclass the ModelHook class and register it in
80 | the global webhook registry. For now we'll set ``async`` to False, this means
81 | the tasks won't be sent to ``celeryd`` but executed locally instead. In
82 | production you would certainly want the dispatch to be asynchronous.
83 |
84 | >>> from durian.event import ModelHook
85 | >>> from durian.registry import hooks
86 | >>> from django.db.models import signals
87 |
88 |
89 | >>> class PersonHook(ModelHook):
90 | ... name = "person"
91 | ... model = Person
92 | ... signal = signals.post_save
93 | ... provides_args = ["name", "address"]
94 | ... async = False
95 | >>> hooks.register(PersonHook)
96 |
97 | Now we can create ourselves some listeners. They can be created manually
98 | or by using the web-interface. A listener must have a URL, which is the
99 | destination callback the signal is sent to, and you can optionally filter
100 | events so you only get the events you care about.
101 |
102 | >>> # send event when person with name Joe is changed/added.
103 | >>> PersonHook().listener(
104 | ... url="http://where.joe/is/listening").match(
105 | ... name="Joe").save()
106 |
107 | >>> # send event whenever a person with a name that starts with the
108 | >>> # letter "J" is changed/added:
109 | >>> from durian.match import Startswith
110 | >>> PersonHook().listener(
111 | ... url="http://where.joe/is/listening").match(
112 | ... name=Startswith("J").save()
113 |
114 | >>> # send event when any Person is changed/added.
115 | >>> PersonHook().listener(url="http://where.joe/is/listening").save()
116 |
117 | The filters can use special matching classes, as you see with the
118 | ``Startswith`` above. See `Matching classes`_ for a list of these.
119 |
120 | In this screenshot you can see the view for selecting the person event:
121 |
122 | .. image::
123 | http://cloud.github.com/downloads/ask/durian/durian-shot-select_event.png
124 |
125 | and then creating a listener for that event:
126 |
127 | .. image::
128 | http://cloud.github.com/downloads/ask/durian/durian-shot-create_listenerv2.png
129 |
130 |
131 | Creating custom hooks
132 | ---------------------
133 |
134 | Sometimes you'd like to create hooks for something else than a model.
135 | If there's already a Django signal you want to bind to there is the
136 | ``SignalHook``. Otherwise you can send your own signal by creating a custom
137 | ``Hook``.
138 |
139 | The only required attribute of a hook is the name, so it can be uniquely
140 | identified in the hook registry.
141 |
142 | There are two ways of defining a hook, either by instantiation a Hook
143 | class, or by subclassing one. You can register a hook instance, or a hook
144 | class, it doesn't matter as long as the name is different:
145 |
146 | >>> from durian.registry import hooks
147 |
148 | >>> # Defining a hook by instantiating a hook class:
149 | >>> myhook = Hook(name="myhook")
150 | >>> hooks.register(myhook)
151 |
152 | >>> # Defining a hook by subclassing a hook class:
153 | >>> class MyHook(Hook):
154 | ... name = "myhook"
155 | >>> hooks.register(MyHook)
156 |
157 |
158 | These also supports the ``provides_args`` attribute which can automatically
159 | generate event filter forms.
160 |
161 | See the API reference for a complete list of ``Hook`` arguments and
162 | attributes.
163 |
164 | Triggering a hook is simple by using the ``send`` method::
165 |
166 | >>> class MyHook(Hook):
167 | ... name = "myhook"
168 | ... provides_args = ["name", "address"]
169 | ... async = False
170 | >>> hooks.register(MyHook)
171 |
172 | >>> MyHook().send(sender=None,
173 | ... name="George Constanza", address="New York City")
174 |
175 |
176 | View for listening URL
177 | ----------------------
178 |
179 | >>> from django.http import HttpResponse
180 | >>> from anyjson import deserialize
181 |
182 | >>> def listens(request):
183 | ... payload = deserialize(request.raw_post_data)
184 | ... print(payload["name"])
185 | ... return HttpResponse("thanks!")
186 |
187 |
188 | Matching classes
189 | ----------------
190 |
191 |
192 | * Any()
193 | Matches anything. Even if the field is not sent at all.
194 |
195 | * Is(pattern)
196 | Strict equality. The values must match precisely.
197 |
198 | * Startswith(pattern)
199 | Matches if the string starts with the given pattern.
200 |
201 | * Endswith(pattern)
202 | Matches if the string ends with the given pattern
203 |
204 | * Contains(pattern)
205 | Matches if the string contains the given pattern.
206 |
207 | * Like(regex)
208 | Match by a regular expression.
209 |
210 |
211 |
212 | Installation
213 | ============
214 |
215 | You can install ``durian`` either via the Python Package Index (PyPI)
216 | or from source.
217 |
218 | To install using ``pip``,::
219 |
220 | $ pip install durian
221 |
222 |
223 | To install using ``easy_install``,::
224 |
225 | $ easy_install durian
226 |
227 |
228 | If you have downloaded a source tarball you can install it
229 | by doing the following,::
230 |
231 | $ python setup.py build
232 | # python setup.py install # as root
233 |
234 | Examples
235 | ========
236 |
237 | .. Please write some examples using your package here.
238 |
239 |
240 | License
241 | =======
242 |
243 | BSD License
244 |
245 |
246 | Contact
247 | =======
248 |
249 | Ask Solem
250 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | See http://bugs.opera.com/browse/OPAL
2 |
--------------------------------------------------------------------------------
/contrib/doc2ghpages:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | git checkout master
4 | (cd docs;
5 | rm -rf .build;
6 | make html;
7 | (cd .build/html;
8 | sphinx-to-github;))
9 | git checkout gh-pages
10 | cp -r docs/.build/html/* .
11 | git commit . -m "Autogenerated documentation for github."
12 | git push --all
13 | git checkout master
14 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 |
9 | # Internal variables.
10 | PAPEROPT_a4 = -D latex_paper_size=a4
11 | PAPEROPT_letter = -D latex_paper_size=letter
12 | ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
13 |
14 | .PHONY: help clean html web pickle htmlhelp latex changes linkcheck
15 |
16 | help:
17 | @echo "Please use \`make ' where is one of"
18 | @echo " html to make standalone HTML files"
19 | @echo " pickle to make pickle files"
20 | @echo " json to make JSON files"
21 | @echo " htmlhelp to make HTML files and a HTML help project"
22 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
23 | @echo " changes to make an overview over all changed/added/deprecated items"
24 | @echo " linkcheck to check all external links for integrity"
25 |
26 | clean:
27 | -rm -rf .build/*
28 |
29 | html:
30 | mkdir -p .build/html .build/doctrees
31 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html
32 | @echo
33 | @echo "Build finished. The HTML pages are in .build/html."
34 |
35 | pickle:
36 | mkdir -p .build/pickle .build/doctrees
37 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle
38 | @echo
39 | @echo "Build finished; now you can process the pickle files."
40 |
41 | web: pickle
42 |
43 | json:
44 | mkdir -p .build/json .build/doctrees
45 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) .build/json
46 | @echo
47 | @echo "Build finished; now you can process the JSON files."
48 |
49 | htmlhelp:
50 | mkdir -p .build/htmlhelp .build/doctrees
51 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp
52 | @echo
53 | @echo "Build finished; now you can run HTML Help Workshop with the" \
54 | ".hhp project file in .build/htmlhelp."
55 |
56 | latex:
57 | mkdir -p .build/latex .build/doctrees
58 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex
59 | @echo
60 | @echo "Build finished; the LaTeX files are in .build/latex."
61 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
62 | "run these through (pdf)latex."
63 |
64 | changes:
65 | mkdir -p .build/changes .build/doctrees
66 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes
67 | @echo
68 | @echo "The overview file is in .build/changes."
69 |
70 | linkcheck:
71 | mkdir -p .build/linkcheck .build/doctrees
72 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck
73 | @echo
74 | @echo "Link check complete; look for any errors in the above output " \
75 | "or in .build/linkcheck/output.txt."
76 |
--------------------------------------------------------------------------------
/docs/_ext/applyxrefs.py:
--------------------------------------------------------------------------------
1 | """Adds xref targets to the top of files."""
2 |
3 | import sys
4 | import os
5 |
6 | testing = False
7 |
8 | DONT_TOUCH = (
9 | './index.txt',
10 | )
11 |
12 |
13 | def target_name(fn):
14 | if fn.endswith('.txt'):
15 | fn = fn[:-4]
16 | return '_' + fn.lstrip('./').replace('/', '-')
17 |
18 |
19 | def process_file(fn, lines):
20 | lines.insert(0, '\n')
21 | lines.insert(0, '.. %s:\n' % target_name(fn))
22 | try:
23 | f = open(fn, 'w')
24 | except IOError:
25 | print("Can't open %s for writing. Not touching it." % fn)
26 | return
27 | try:
28 | f.writelines(lines)
29 | except IOError:
30 | print("Can't write to %s. Not touching it." % fn)
31 | finally:
32 | f.close()
33 |
34 |
35 | def has_target(fn):
36 | try:
37 | f = open(fn, 'r')
38 | except IOError:
39 | print("Can't open %s. Not touching it." % fn)
40 | return (True, None)
41 | readok = True
42 | try:
43 | lines = f.readlines()
44 | except IOError:
45 | print("Can't read %s. Not touching it." % fn)
46 | readok = False
47 | finally:
48 | f.close()
49 | if not readok:
50 | return (True, None)
51 |
52 | #print fn, len(lines)
53 | if len(lines) < 1:
54 | print("Not touching empty file %s." % fn)
55 | return (True, None)
56 | if lines[0].startswith('.. _'):
57 | return (True, None)
58 | return (False, lines)
59 |
60 |
61 | def main(argv=None):
62 | if argv is None:
63 | argv = sys.argv
64 |
65 | if len(argv) == 1:
66 | argv.extend('.')
67 |
68 | files = []
69 | for root in argv[1:]:
70 | for (dirpath, dirnames, filenames) in os.walk(root):
71 | files.extend([(dirpath, f) for f in filenames])
72 | files.sort()
73 | files = [os.path.join(p, fn) for p, fn in files if fn.endswith('.txt')]
74 | #print files
75 |
76 | for fn in files:
77 | if fn in DONT_TOUCH:
78 | print("Skipping blacklisted file %s." % fn)
79 | continue
80 |
81 | target_found, lines = has_target(fn)
82 | if not target_found:
83 | if testing:
84 | print '%s: %s' % (fn, lines[0]),
85 | else:
86 | print "Adding xref to %s" % fn
87 | process_file(fn, lines)
88 | else:
89 | print "Skipping %s: already has a xref" % fn
90 |
91 | if __name__ == '__main__':
92 | sys.exit(main())
93 |
--------------------------------------------------------------------------------
/docs/_ext/literals_to_xrefs.py:
--------------------------------------------------------------------------------
1 | """
2 | Runs through a reST file looking for old-style literals, and helps replace them
3 | with new-style references.
4 | """
5 |
6 | import re
7 | import sys
8 | import shelve
9 |
10 | refre = re.compile(r'``([^`\s]+?)``')
11 |
12 | ROLES = (
13 | 'attr',
14 | 'class',
15 | "djadmin",
16 | 'data',
17 | 'exc',
18 | 'file',
19 | 'func',
20 | 'lookup',
21 | 'meth',
22 | 'mod',
23 | "djadminopt",
24 | "ref",
25 | "setting",
26 | "term",
27 | "tfilter",
28 | "ttag",
29 |
30 | # special
31 | "skip",
32 | )
33 |
34 | ALWAYS_SKIP = [
35 | "NULL",
36 | "True",
37 | "False",
38 | ]
39 |
40 |
41 | def fixliterals(fname):
42 | data = open(fname).read()
43 |
44 | last = 0
45 | new = []
46 | storage = shelve.open("/tmp/literals_to_xref.shelve")
47 | lastvalues = storage.get("lastvalues", {})
48 |
49 | for m in refre.finditer(data):
50 |
51 | new.append(data[last:m.start()])
52 | last = m.end()
53 |
54 | line_start = data.rfind("\n", 0, m.start())
55 | line_end = data.find("\n", m.end())
56 | prev_start = data.rfind("\n", 0, line_start)
57 | next_end = data.find("\n", line_end + 1)
58 |
59 | # Skip always-skip stuff
60 | if m.group(1) in ALWAYS_SKIP:
61 | new.append(m.group(0))
62 | continue
63 |
64 | # skip when the next line is a title
65 | next_line = data[m.end():next_end].strip()
66 | if next_line[0] in "!-/:-@[-`{-~" and \
67 | all(c == next_line[0] for c in next_line):
68 | new.append(m.group(0))
69 | continue
70 |
71 | sys.stdout.write("\n"+"-"*80+"\n")
72 | sys.stdout.write(data[prev_start+1:m.start()])
73 | sys.stdout.write(colorize(m.group(0), fg="red"))
74 | sys.stdout.write(data[m.end():next_end])
75 | sys.stdout.write("\n\n")
76 |
77 | replace_type = None
78 | while replace_type is None:
79 | replace_type = raw_input(
80 | colorize("Replace role: ", fg="yellow")).strip().lower()
81 | if replace_type and replace_type not in ROLES:
82 | replace_type = None
83 |
84 | if replace_type == "":
85 | new.append(m.group(0))
86 | continue
87 |
88 | if replace_type == "skip":
89 | new.append(m.group(0))
90 | ALWAYS_SKIP.append(m.group(1))
91 | continue
92 |
93 | default = lastvalues.get(m.group(1), m.group(1))
94 | if default.endswith("()") and \
95 | replace_type in ("class", "func", "meth"):
96 | default = default[:-2]
97 | replace_value = raw_input(
98 | colorize("Text [", fg="yellow") + default + \
99 | colorize("]: ", fg="yellow")).strip()
100 | if not replace_value:
101 | replace_value = default
102 | new.append(":%s:`%s`" % (replace_type, replace_value))
103 | lastvalues[m.group(1)] = replace_value
104 |
105 | new.append(data[last:])
106 | open(fname, "w").write("".join(new))
107 |
108 | storage["lastvalues"] = lastvalues
109 | storage.close()
110 |
111 |
112 | def colorize(text='', opts=(), **kwargs):
113 | """
114 | Returns your text, enclosed in ANSI graphics codes.
115 |
116 | Depends on the keyword arguments 'fg' and 'bg', and the contents of
117 | the opts tuple/list.
118 |
119 | Returns the RESET code if no parameters are given.
120 |
121 | Valid colors:
122 | 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'
123 |
124 | Valid options:
125 | 'bold'
126 | 'underscore'
127 | 'blink'
128 | 'reverse'
129 | 'conceal'
130 | 'noreset' - string will not be auto-terminated with the RESET code
131 |
132 | Examples:
133 | colorize('hello', fg='red', bg='blue', opts=('blink',))
134 | colorize()
135 | colorize('goodbye', opts=('underscore',))
136 | print colorize('first line', fg='red', opts=('noreset',))
137 | print 'this should be red too'
138 | print colorize('and so should this')
139 | print 'this should not be red'
140 | """
141 | color_names = ('black', 'red', 'green', 'yellow',
142 | 'blue', 'magenta', 'cyan', 'white')
143 | foreground = dict([(color_names[x], '3%s' % x) for x in range(8)])
144 | background = dict([(color_names[x], '4%s' % x) for x in range(8)])
145 |
146 | RESET = '0'
147 | opt_dict = {'bold': '1',
148 | 'underscore': '4',
149 | 'blink': '5',
150 | 'reverse': '7',
151 | 'conceal': '8'}
152 |
153 | text = str(text)
154 | code_list = []
155 | if text == '' and len(opts) == 1 and opts[0] == 'reset':
156 | return '\x1b[%sm' % RESET
157 | for k, v in kwargs.iteritems():
158 | if k == 'fg':
159 | code_list.append(foreground[v])
160 | elif k == 'bg':
161 | code_list.append(background[v])
162 | for o in opts:
163 | if o in opt_dict:
164 | code_list.append(opt_dict[o])
165 | if 'noreset' not in opts:
166 | text = text + '\x1b[%sm' % RESET
167 | return ('\x1b[%sm' % ';'.join(code_list)) + text
168 |
169 | if __name__ == '__main__':
170 | try:
171 | fixliterals(sys.argv[1])
172 | except (KeyboardInterrupt, SystemExit):
173 | print
174 |
--------------------------------------------------------------------------------
/docs/_theme/nature/static/nature.css_t:
--------------------------------------------------------------------------------
1 | /**
2 | * Sphinx stylesheet -- default theme
3 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 | */
5 |
6 | @import url("basic.css");
7 |
8 | /* -- page layout ----------------------------------------------------------- */
9 |
10 | body {
11 | font-family: Arial, sans-serif;
12 | font-size: 100%;
13 | background-color: #111;
14 | color: #555;
15 | margin: 0;
16 | padding: 0;
17 | }
18 |
19 | hr{
20 | border: 1px solid #B1B4B6;
21 | }
22 |
23 | div.document {
24 | background-color: #eee;
25 | }
26 |
27 | div.body {
28 | background-color: #ffffff;
29 | color: #3E4349;
30 | padding: 0 30px 30px 30px;
31 | font-size: 0.8em;
32 | }
33 |
34 | div.footer {
35 | color: #555;
36 | width: 100%;
37 | padding: 13px 0;
38 | text-align: center;
39 | font-size: 75%;
40 | }
41 |
42 | div.footer a {
43 | color: #444;
44 | text-decoration: underline;
45 | }
46 |
47 | div.related {
48 | background-color: #6BA81E;
49 | line-height: 32px;
50 | color: #fff;
51 | text-shadow: 0px 1px 0 #444;
52 | font-size: 0.80em;
53 | }
54 |
55 | div.related a {
56 | color: #E2F3CC;
57 | }
58 |
59 | div.sphinxsidebar {
60 | font-size: 0.75em;
61 | line-height: 1.5em;
62 | }
63 |
64 | div.sphinxsidebarwrapper{
65 | padding: 20px 0;
66 | }
67 |
68 | div.sphinxsidebar h3,
69 | div.sphinxsidebar h4 {
70 | font-family: Arial, sans-serif;
71 | color: #222;
72 | font-size: 1.2em;
73 | font-weight: normal;
74 | margin: 0;
75 | padding: 5px 10px;
76 | background-color: #ddd;
77 | text-shadow: 1px 1px 0 white
78 | }
79 |
80 | div.sphinxsidebar h4{
81 | font-size: 1.1em;
82 | }
83 |
84 | div.sphinxsidebar h3 a {
85 | color: #444;
86 | }
87 |
88 |
89 | div.sphinxsidebar p {
90 | color: #888;
91 | padding: 5px 20px;
92 | }
93 |
94 | div.sphinxsidebar p.topless {
95 | }
96 |
97 | div.sphinxsidebar ul {
98 | margin: 10px 20px;
99 | padding: 0;
100 | color: #000;
101 | }
102 |
103 | div.sphinxsidebar a {
104 | color: #444;
105 | }
106 |
107 | div.sphinxsidebar input {
108 | border: 1px solid #ccc;
109 | font-family: sans-serif;
110 | font-size: 1em;
111 | }
112 |
113 | div.sphinxsidebar input[type=text]{
114 | margin-left: 20px;
115 | }
116 |
117 | /* -- body styles ----------------------------------------------------------- */
118 |
119 | a {
120 | color: #005B81;
121 | text-decoration: none;
122 | }
123 |
124 | a:hover {
125 | color: #E32E00;
126 | text-decoration: underline;
127 | }
128 |
129 | div.body h1,
130 | div.body h2,
131 | div.body h3,
132 | div.body h4,
133 | div.body h5,
134 | div.body h6 {
135 | font-family: Arial, sans-serif;
136 | background-color: #BED4EB;
137 | font-weight: normal;
138 | color: #212224;
139 | margin: 30px 0px 10px 0px;
140 | padding: 5px 0 5px 10px;
141 | text-shadow: 0px 1px 0 white
142 | }
143 |
144 | div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
145 | div.body h2 { font-size: 150%; background-color: #C8D5E3; }
146 | div.body h3 { font-size: 120%; background-color: #D8DEE3; }
147 | div.body h4 { font-size: 110%; background-color: #D8DEE3; }
148 | div.body h5 { font-size: 100%; background-color: #D8DEE3; }
149 | div.body h6 { font-size: 100%; background-color: #D8DEE3; }
150 |
151 | a.headerlink {
152 | color: #c60f0f;
153 | font-size: 0.8em;
154 | padding: 0 4px 0 4px;
155 | text-decoration: none;
156 | }
157 |
158 | a.headerlink:hover {
159 | background-color: #c60f0f;
160 | color: white;
161 | }
162 |
163 | div.body p, div.body dd, div.body li {
164 | text-align: justify;
165 | line-height: 1.5em;
166 | }
167 |
168 | div.admonition p.admonition-title + p {
169 | display: inline;
170 | }
171 |
172 | div.highlight{
173 | background-color: white;
174 | }
175 |
176 | div.note {
177 | background-color: #eee;
178 | border: 1px solid #ccc;
179 | }
180 |
181 | div.seealso {
182 | background-color: #ffc;
183 | border: 1px solid #ff6;
184 | }
185 |
186 | div.topic {
187 | background-color: #eee;
188 | }
189 |
190 | div.warning {
191 | background-color: #ffe4e4;
192 | border: 1px solid #f66;
193 | }
194 |
195 | p.admonition-title {
196 | display: inline;
197 | }
198 |
199 | p.admonition-title:after {
200 | content: ":";
201 | }
202 |
203 | pre {
204 | padding: 10px;
205 | background-color: White;
206 | color: #222;
207 | line-height: 1.2em;
208 | border: 1px solid #C6C9CB;
209 | font-size: 1.2em;
210 | margin: 1.5em 0 1.5em 0;
211 | -webkit-box-shadow: 1px 1px 1px #d8d8d8;
212 | -moz-box-shadow: 1px 1px 1px #d8d8d8;
213 | }
214 |
215 | tt {
216 | background-color: #ecf0f3;
217 | color: #222;
218 | padding: 1px 2px;
219 | font-size: 1.2em;
220 | font-family: monospace;
221 | }
--------------------------------------------------------------------------------
/docs/_theme/nature/static/pygments.css:
--------------------------------------------------------------------------------
1 | .c { color: #999988; font-style: italic } /* Comment */
2 | .k { font-weight: bold } /* Keyword */
3 | .o { font-weight: bold } /* Operator */
4 | .cm { color: #999988; font-style: italic } /* Comment.Multiline */
5 | .cp { color: #999999; font-weight: bold } /* Comment.preproc */
6 | .c1 { color: #999988; font-style: italic } /* Comment.Single */
7 | .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
8 | .ge { font-style: italic } /* Generic.Emph */
9 | .gr { color: #aa0000 } /* Generic.Error */
10 | .gh { color: #999999 } /* Generic.Heading */
11 | .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
12 | .go { color: #111 } /* Generic.Output */
13 | .gp { color: #555555 } /* Generic.Prompt */
14 | .gs { font-weight: bold } /* Generic.Strong */
15 | .gu { color: #aaaaaa } /* Generic.Subheading */
16 | .gt { color: #aa0000 } /* Generic.Traceback */
17 | .kc { font-weight: bold } /* Keyword.Constant */
18 | .kd { font-weight: bold } /* Keyword.Declaration */
19 | .kp { font-weight: bold } /* Keyword.Pseudo */
20 | .kr { font-weight: bold } /* Keyword.Reserved */
21 | .kt { color: #445588; font-weight: bold } /* Keyword.Type */
22 | .m { color: #009999 } /* Literal.Number */
23 | .s { color: #bb8844 } /* Literal.String */
24 | .na { color: #008080 } /* Name.Attribute */
25 | .nb { color: #999999 } /* Name.Builtin */
26 | .nc { color: #445588; font-weight: bold } /* Name.Class */
27 | .no { color: #ff99ff } /* Name.Constant */
28 | .ni { color: #800080 } /* Name.Entity */
29 | .ne { color: #990000; font-weight: bold } /* Name.Exception */
30 | .nf { color: #990000; font-weight: bold } /* Name.Function */
31 | .nn { color: #555555 } /* Name.Namespace */
32 | .nt { color: #000080 } /* Name.Tag */
33 | .nv { color: purple } /* Name.Variable */
34 | .ow { font-weight: bold } /* Operator.Word */
35 | .mf { color: #009999 } /* Literal.Number.Float */
36 | .mh { color: #009999 } /* Literal.Number.Hex */
37 | .mi { color: #009999 } /* Literal.Number.Integer */
38 | .mo { color: #009999 } /* Literal.Number.Oct */
39 | .sb { color: #bb8844 } /* Literal.String.Backtick */
40 | .sc { color: #bb8844 } /* Literal.String.Char */
41 | .sd { color: #bb8844 } /* Literal.String.Doc */
42 | .s2 { color: #bb8844 } /* Literal.String.Double */
43 | .se { color: #bb8844 } /* Literal.String.Escape */
44 | .sh { color: #bb8844 } /* Literal.String.Heredoc */
45 | .si { color: #bb8844 } /* Literal.String.Interpol */
46 | .sx { color: #bb8844 } /* Literal.String.Other */
47 | .sr { color: #808000 } /* Literal.String.Regex */
48 | .s1 { color: #bb8844 } /* Literal.String.Single */
49 | .ss { color: #bb8844 } /* Literal.String.Symbol */
50 | .bp { color: #999999 } /* Name.Builtin.Pseudo */
51 | .vc { color: #ff99ff } /* Name.Variable.Class */
52 | .vg { color: #ff99ff } /* Name.Variable.Global */
53 | .vi { color: #ff99ff } /* Name.Variable.Instance */
54 | .il { color: #009999 } /* Literal.Number.Integer.Long */
--------------------------------------------------------------------------------
/docs/_theme/nature/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = nature.css
4 | pygments_style = tango
5 |
6 | [options]
7 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | ../Changelog
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | import sys
4 | import os
5 |
6 | # If your extensions are in another directory, add it here. If the directory
7 | # is relative to the documentation root, use os.path.abspath to make it
8 | # absolute, like shown here.
9 | sys.path.insert(0, "../")
10 | import durian
11 |
12 | from django.conf import settings
13 | if not settings.configured:
14 | settings.configure()
15 |
16 | # General configuration
17 | # ---------------------
18 |
19 | # Add any Sphinx extension module names here, as strings.
20 | # They can be extensions
21 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
22 | extensions = ['sphinx.ext.autodoc']
23 |
24 | # Add any paths that contain templates here, relative to this directory.
25 | templates_path = ['.templates']
26 |
27 | # The suffix of source filenames.
28 | source_suffix = '.rst'
29 |
30 | # The encoding of source files.
31 | #source_encoding = 'utf-8'
32 |
33 | # The master toctree document.
34 | master_doc = 'index'
35 |
36 | # General information about the project.
37 | project = u'durian'
38 | copyright = u'2009, Opera Softare (WebTeam)'
39 |
40 | # The version info for the project you're documenting, acts as replacement for
41 | # |version| and |release|, also used in various other places throughout the
42 | # built documents.
43 | #
44 | # The short X.Y version.
45 | version = ".".join(map(str, durian.VERSION[0:2]))
46 | # The full version, including alpha/beta/rc tags.
47 | release = durian.__version__
48 |
49 | # The language for content autogenerated by Sphinx. Refer to documentation
50 | # for a list of supported languages.
51 | #language = None
52 |
53 | # There are two options for replacing |today|: either, you set today to some
54 | # non-false value, then it is used:
55 | #today = ''
56 | # Else, today_fmt is used as the format for a strftime call.
57 | #today_fmt = '%B %d, %Y'
58 |
59 | # List of documents that shouldn't be included in the build.
60 | #unused_docs = []
61 |
62 | # List of directories, relative to source directory, that shouldn't be searched
63 | # for source files.
64 | exclude_trees = ['.build']
65 |
66 | # The reST default role (used for this markup: `text`) to use for all
67 | # documents.
68 | #default_role = None
69 |
70 | # If true, '()' will be appended to :func: etc. cross-reference text.
71 | add_function_parentheses = True
72 |
73 | # If true, the current module name will be prepended to all description
74 | # unit titles (such as .. function::).
75 | #add_module_names = True
76 |
77 | # If true, sectionauthor and moduleauthor directives will be shown in the
78 | # output. They are ignored by default.
79 | #show_authors = False
80 |
81 | # The name of the Pygments (syntax highlighting) style to use.
82 | pygments_style = 'trac'
83 |
84 | #html_translator_class = "djangodocs.DjangoHTMLTranslator"
85 |
86 |
87 | # Options for HTML output
88 | # -----------------------
89 |
90 | # The style sheet to use for HTML and HTML Help pages. A file of that name
91 | # must exist either in Sphinx' static/ path, or in one of the custom paths
92 | # given in html_static_path.
93 | #html_style = 'agogo.css'
94 |
95 | # The name for this set of Sphinx documents. If None, it defaults to
96 | # " v documentation".
97 | #html_title = None
98 |
99 | # A shorter title for the navigation bar. Default is the same as html_title.
100 | #html_short_title = None
101 |
102 | # The name of an image file (relative to this directory) to place at the top
103 | # of the sidebar.
104 | #html_logo = None
105 |
106 | # The name of an image file (within the static path) to use as favicon of the
107 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
108 | # pixels large.
109 | #html_favicon = None
110 |
111 | # Add any paths that contain custom static files (such as style sheets) here,
112 | # relative to this directory. They are copied after the builtin static files,
113 | # so a file named "default.css" will overwrite the builtin "default.css".
114 | html_static_path = ['.static']
115 |
116 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
117 | # using the given strftime format.
118 | #html_last_updated_fmt = '%b %d, %Y'
119 |
120 | # If true, SmartyPants will be used to convert quotes and dashes to
121 | # typographically correct entities.
122 | html_use_smartypants = True
123 |
124 | # Custom sidebar templates, maps document names to template names.
125 | #html_sidebars = {}
126 |
127 | # Additional templates that should be rendered to pages, maps page names to
128 | # template names.
129 | #html_additional_pages = {}
130 |
131 | # If false, no module index is generated.
132 | html_use_modindex = True
133 |
134 | # If false, no index is generated.
135 | html_use_index = True
136 |
137 | # If true, the index is split into individual pages for each letter.
138 | #html_split_index = False
139 |
140 | # If true, the reST sources are included in the HTML build as _sources/.
141 | #html_copy_source = True
142 |
143 | # If true, an OpenSearch description file will be output, and all pages will
144 | # contain a tag referring to it. The value of this option must be the
145 | # base URL from which the finished HTML is served.
146 | #html_use_opensearch = ''
147 |
148 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
149 | #html_file_suffix = ''
150 |
151 | # Output file base name for HTML help builder.
152 | htmlhelp_basename = 'duriandoc'
153 |
154 |
155 | # Options for LaTeX output
156 | # ------------------------
157 |
158 | # The paper size ('letter' or 'a4').
159 | #latex_paper_size = 'letter'
160 |
161 | # The font size ('10pt', '11pt' or '12pt').
162 | #latex_font_size = '10pt'
163 |
164 | # Grouping the document tree into LaTeX files. List of tuples
165 | # (source start file, target name, title, author, document class
166 | # [howto/manual]).
167 | latex_documents = [
168 | ('index', 'durian.tex', ur'durian Documentation',
169 | ur'Ask Solem', 'manual'),
170 | ]
171 |
172 | # The name of an image file (relative to this directory) to place at the top of
173 | # the title page.
174 | #latex_logo = None
175 |
176 | # For "manual" documents, if this is true, then toplevel headings are parts,
177 | # not chapters.
178 | #latex_use_parts = False
179 |
180 | # Additional stuff for the LaTeX preamble.
181 | #latex_preamble = ''
182 |
183 | # Documents to append as an appendix to all manuals.
184 | #latex_appendices = []
185 |
186 | # If false, no module index is generated.
187 | #latex_use_modindex = True
188 |
189 | html_theme = "nature"
190 | html_theme_path = ["_theme"]
191 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Durian Documentation
2 | ==================================
3 |
4 | Contents:
5 |
6 | .. toctree::
7 | :maxdepth: 2
8 |
9 | introduction
10 | reference/index
11 | changelog
12 |
13 |
14 | Indices and tables
15 | ==================
16 |
17 | * :ref:`genindex`
18 | * :ref:`modindex`
19 | * :ref:`search`
20 |
21 |
--------------------------------------------------------------------------------
/docs/introduction.rst:
--------------------------------------------------------------------------------
1 | ../README
--------------------------------------------------------------------------------
/docs/reference/durian.event.rst:
--------------------------------------------------------------------------------
1 | ====================================
2 | Defining Hooks - durian.event
3 | ====================================
4 |
5 | .. currentmodule:: durian.event
6 |
7 | .. automodule:: durian.event
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/reference/durian.forms.rst:
--------------------------------------------------------------------------------
1 | ==================================
2 | Django Forms - durian.forms
3 | ==================================
4 |
5 | .. currentmodule:: durian.forms
6 |
7 | .. automodule:: durian.forms
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/reference/durian.match.able.rst:
--------------------------------------------------------------------------------
1 | ==============================================
2 | Matching Conditions - durian.match.able
3 | ==============================================
4 |
5 | .. currentmodule:: durian.match.able
6 |
7 | .. automodule:: durian.match.able
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/reference/durian.match.rst:
--------------------------------------------------------------------------------
1 | ==================================================
2 | Matching/Filtering Utilities - durian.match
3 | ==================================================
4 |
5 | .. currentmodule:: durian.match
6 |
7 | .. automodule:: durian.match
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/reference/durian.match.strategy.rst:
--------------------------------------------------------------------------------
1 | ==================================================
2 | Matching Strategies - durian.match.strategy
3 | ==================================================
4 |
5 | .. currentmodule:: durian.match.strategy
6 |
7 | .. automodule:: durian.match.strategy
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/reference/durian.models.rst:
--------------------------------------------------------------------------------
1 | ====================================
2 | Django Models - durian.models
3 | ====================================
4 |
5 | .. currentmodule:: durian.models
6 |
7 | .. automodule:: durian.models
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/reference/durian.tasks.rst:
--------------------------------------------------------------------------------
1 | ==================================
2 | Celery Tasks - durian.tasks
3 | ==================================
4 |
5 | .. currentmodule:: durian.tasks
6 |
7 | .. automodule:: durian.tasks
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/reference/durian.views.rst:
--------------------------------------------------------------------------------
1 | ==================================
2 | Django Views - durian.views
3 | ==================================
4 |
5 | .. currentmodule:: durian.views
6 |
7 | .. automodule:: durian.views
8 | :members:
9 |
--------------------------------------------------------------------------------
/docs/reference/index.rst:
--------------------------------------------------------------------------------
1 | ===========================
2 | Module API Reference
3 | ===========================
4 |
5 | :Release: |version|
6 | :Date: |today|
7 |
8 | .. toctree::
9 | :maxdepth: 2
10 |
11 | durian.event
12 | durian.tasks
13 | durian.views
14 | durian.forms
15 | durian.models
16 | durian.match
17 | durian.match.able
18 | durian.match.strategy
19 |
--------------------------------------------------------------------------------
/durian/__init__.py:
--------------------------------------------------------------------------------
1 | """Webhooks for Django **EXPERIMENTAL**"""
2 | VERSION = (0, 1, 0)
3 | __version__ = ".".join(map(str, VERSION))
4 | __author__ = "Ask Solem"
5 | __contact__ = "askh@opera.com"
6 | __homepage__ = "http://github.com/ask/durian/"
7 | __docformat__ = "restructuredtext"
8 |
--------------------------------------------------------------------------------
/durian/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from durian.models import Listener
3 |
4 | admin.site.register(Listener)
5 |
--------------------------------------------------------------------------------
/durian/event.py:
--------------------------------------------------------------------------------
1 | from durian.models import Listener
2 | from celery.utils import get_full_cls_name, gen_unique_id
3 | from durian.tasks import WebhookSignal
4 | from durian.forms import HookConfigForm, create_match_forms
5 | from durian.match.strategy import deepmatch
6 | from durian.match import mtuplelist_to_matchdict
7 | from functools import partial as curry
8 |
9 |
10 | class Hook(object):
11 | """A Web Hook Event.
12 |
13 | :keyword name: See :attr:`name`.
14 | :keyword provides_args: See :attr:`provides_args`.
15 | :keyword config_form: See :attr:`config_form`.
16 | :keyword timeout: See :attr:`timeout`.
17 | :keyword async: See :attr:`async`.
18 | :keyword retry: See :attr:`retry`.
19 | :keyword max_retries: See :attr:`max_retries`.
20 | :keyword fail_silently: See :attr:`fail_silently`.
21 | :keyword task_cls: See :attr:`task_cls`.
22 | :keyword match_forms: See :attr:`match_forms`
23 |
24 | .. attribute:: name
25 |
26 | The name of the hook.
27 |
28 | If not provided this will be automatically generated using the class
29 | module and name, if you want to use this feature you can't use
30 | relative imports.
31 |
32 | .. attribute:: provides_args
33 |
34 | The list of arguments the event provides. This is the standard
35 | list of arguments you are going to pass on to :meth:`send`, used to
36 | generate the filter events form (:attr:`match_forms`).
37 |
38 | .. attribute:: config_form
39 |
40 | A Django form to save configuration for listeners attaching to this
41 | form. The default form is :class:`durian.forms.HookConfigForm`, which
42 | has the URL field.
43 |
44 | .. attribute:: timeout
45 |
46 | The timeout in seconds before we give up trying to dispatch the
47 | event to a listener URL.
48 |
49 | .. attribute:: async
50 |
51 | If ``True``, signals are dispatched to celery workers via a mesage.
52 | Otherwise dispatch happens locally (not a good idea in production).
53 |
54 | .. attribute:: retry
55 |
56 | Retry the task if it fails.
57 |
58 | .. attribute:: max_retries
59 |
60 | Maximum number of retries before we give up.
61 |
62 | .. attribute:: fail_silently
63 |
64 | Fail silently if the dispatch gives an HTTP error.
65 |
66 | .. attribute:: task_cls
67 |
68 | The :class:`celery.task.base.Task` class to use for dispatching
69 | the event.
70 |
71 | .. attribute:: match_forms
72 |
73 | A list of forms to create an event filter. This is automatically
74 | generated based on the :attr:`provides_args` attribute.
75 |
76 | """
77 |
78 | name = None
79 | verbose_name = None
80 | task_cls = WebhookSignal
81 | timeout = 4
82 | async = True
83 | retry = False
84 | max_retries = 3
85 | fail_silently = False
86 | config_form = HookConfigForm
87 | provides_args = set()
88 | match_forms = None
89 |
90 | def __init__(self, name=None, verbose_name=None, task_cls=None,
91 | timeout=None, async=None, retry=None, max_retries=None,
92 | fail_silently=False, config_form=None, provides_args=None,
93 | match_forms=None, **kwargs):
94 | self.name = name or self.name or get_full_cls_name(self.__class__)
95 | self.verbose_name = verbose_name or self.verbose_name or self.name
96 | self.task_cls = task_cls or self.task_cls
97 | if timeout is not None:
98 | self.timeout = timeout
99 | if async is not None:
100 | self.async = async
101 | if retry is not None:
102 | self.retry = retry
103 | if max_retries is not None:
104 | self.max_retries = max_retries
105 | if fail_silently is not None:
106 | self.fail_silently = fail_silently
107 | self.provides_args = set(provides_args) or self.provides_args
108 | self.config_form = config_form or self.config_form
109 | form_name = "%sConfigForm" % self.name.capitalize()
110 | self.match_forms = match_forms or self.match_forms or \
111 | create_match_forms(form_name, self.provides_args)
112 |
113 | def send(self, sender, **payload):
114 | """Send signal and dispatch to all listeners.
115 |
116 | :param sender: The sender of the signal. Either a specific object
117 | or ``None``.
118 |
119 | :param payload: The data to pass on to listeners. Usually the keys
120 | described in :attr:`provides_args` and any additional keys you'd
121 | want to provide.
122 |
123 | """
124 | payload = self.prepare_payload(sender, payload)
125 | apply_ = curry(self._send_signal, sender, payload)
126 | return map(apply_, self.get_listeners(sender, payload))
127 |
128 | def _send_signal(self, sender, payload, target):
129 | applier = self.get_applier()
130 | return applier(args=[target.url, payload], kwargs=self.task_keywords)
131 |
132 | def event_filter(self, sender, payload, match):
133 | """How we filter events.
134 |
135 | :param sender: The sender of the signal.
136 |
137 | :param payload: The signal data.
138 |
139 | :param match: The match dictionary, or ``None``.
140 |
141 | """
142 | if not match:
143 | return True
144 | return deepmatch(match, payload)
145 |
146 | def get_match_forms(self, **kwargs):
147 | """Initialize the match forms with data recived by a request.
148 |
149 | :returns: A list of instantiated match forms.
150 |
151 | """
152 | return [match_form(**kwargs)
153 | for match_form in self.match_forms.values()]
154 |
155 | def apply_match_forms(self, data):
156 | """With data recieved by request, convert to a list of match
157 | tuples."""
158 | mtuplelist = [match_form(data).field_to_mtuple()
159 | for match_form in self.match_forms.values()]
160 | return mtuplelist_to_matchdict(mtuplelist)
161 |
162 | def get_listeners(self, sender, payload):
163 | """Get a list of all the listeners who wants this signal."""
164 | possible_targets = Listener.objects.filter(hook=self.name)
165 | return [target for target in possible_targets
166 | if self.event_filter(sender, payload, target.match)]
167 |
168 | def get_applier(self, async=None):
169 | """Get the current apply method. Asynchronous or synchronous."""
170 | async = async or self.async
171 | method = "apply_async" if async else "apply"
172 | sender = getattr(self.task_cls, method)
173 | return sender
174 |
175 | def prepare_payload(self, sender, payload):
176 | """Prepare the payload for dispatching.
177 |
178 | You can add any additional formatting of the payload here.
179 |
180 | """
181 | return payload
182 |
183 | def add_listener_by_form(self, form, match=None):
184 | """Add listener with an instantiated :attr:`config_form`.
185 |
186 | :param form: An instance of :attr:`config_form`.
187 | :param match: Optional event filter match dict.
188 |
189 | """
190 | if not hasattr(form, "cleaned_data"):
191 | form.is_valid()
192 | config = dict(form.cleaned_data)
193 | url = config.pop("url")
194 | return Listener.objects.create(hook=self.name, url=url,
195 | match=match, config=config)
196 |
197 | def add_listener(self, url, match={}, **config):
198 | """Add listener for this signal.
199 |
200 | :param url: The url the listener is listening on.
201 | :keyword match: The even filter match dict.
202 | :keyword \*\*config: Hook specific listener configuration.
203 |
204 | """
205 | return Listener.objects.create(hook=self, url=url, match=match,
206 | **dict(config))
207 |
208 | def listener(self, form):
209 | """Create a new listener."""
210 | return IntermediateListener(self, form)
211 |
212 | @property
213 | def task_keywords(self):
214 | """The keyword arguments sent to the celery task."""
215 | return {"retry": self.retry,
216 | "max_retries": self.max_retries,
217 | "fail_silently": self.fail_silently,
218 | "timeout": self.timeout}
219 |
220 |
221 | class SignalHook(Hook):
222 | """Hook attached to a Django signal."""
223 | signal = None
224 | _dispatch_uid = None
225 |
226 | def __init__(self, signal=None, **kwargs):
227 | self.signal = signal
228 |
229 | # Signal receivers must have a unique id, by default
230 | # they're generated by the reciver name and the sender,
231 | # but since it's possible to have different recieves for the
232 | # same instance, we need to generate our own unique id.
233 | if not self.__class__._dispatch_uid:
234 | self.__class__._dispatch_uid = gen_unique_id()
235 |
236 | super(SignalHook, self).__init__(**kwargs)
237 |
238 | def connect(self, sender):
239 | self.signal.connect(self.send, sender=sender,
240 | dispatch_uid=self.__class__._dispatch_uid)
241 |
242 | def disconnect(self, sender):
243 | self.signal.disconnect(self.send, sender=sender,
244 | dispatch_uid=self.__class__._dispatch_uid)
245 |
246 |
247 | class ModelHook(SignalHook):
248 | """
249 | >>> from django.db import signals
250 | >>> from django.contrib.auth.models import User
251 |
252 | >>> hook = ModelHook(User, signals.post_save,
253 | ... name="user-post-save",
254 | ... provides_args=["username", "is_admin"])
255 | >>> joe = User.objects.get(username="joe")
256 | >>> joe.is_admin = True
257 | >>> joe.save()
258 |
259 | """
260 | model = None
261 |
262 | def __init__(self, model=None, **kwargs):
263 | super(ModelHook, self).__init__(**kwargs)
264 | self.model = model or self.model
265 | if not self.model:
266 | raise NotImplementedError("ModelHook requires a model.")
267 | if self.signal:
268 | self.connect()
269 | if not self.provides_args:
270 | self.provides_args = self.get_model_default_fields()
271 |
272 | def get_model_default_fields(self):
273 | return [field.name
274 | for field in self.model._meta.fields
275 | if field.name != self.model._meta.pk.name]
276 |
277 | def prepare_payload(self, sender, payload):
278 | instance = payload.pop("instance")
279 | payload.pop("signal", None)
280 | model_data = dict((field_name, getattr(instance, field_name, None))
281 | for field_name in self.provides_args)
282 | model_data.update(payload)
283 | return model_data
284 |
285 | def connect(self):
286 | super(ModelHook, self).connect(self.model)
287 |
288 | def disconnect(self):
289 | super(ModelHook, self).disconnect(self.model)
290 |
291 |
292 | class IntermediateListener(object):
293 |
294 | def __init__(self, hook, form):
295 | self.hook = hook
296 | self.form = form
297 | self.conditions = None
298 |
299 | def match(self, **match):
300 | self.conditions = match
301 | return self
302 |
303 | def save(self):
304 | return self.hook.add_listener_by_form(self.form, self.conditions)
305 |
--------------------------------------------------------------------------------
/durian/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.translation import ugettext_lazy as _
3 | from durian.match import mtuplelist_to_matchdict, MATCHABLE_CHOICES
4 | from durian.registry import hooks
5 |
6 |
7 | class HookConfigForm(forms.Form):
8 | """Form for the default hook config.
9 |
10 | By default listeners only needs an URL, if you want custom configuration
11 | keys you can subclass this form and use that as the hooks
12 | :attr:`durian.event.Hook.config_form` attribute.
13 |
14 | """
15 | url = forms.URLField(label=_(u"Listener URL"), required=True)
16 |
17 | def save(self):
18 | return dict(self.cleaned_data)
19 |
20 |
21 | def create_select_hook_form(*args, **kwargs):
22 | """Dynamically create a form that has a ``ChoiceField`` listing all the
23 | hook types registered in the hook registry."""
24 |
25 | class SelectHookForm(forms.Form):
26 | type = forms.ChoiceField(choices=hooks.as_choices())
27 |
28 | return SelectHookForm(*args, **kwargs)
29 |
30 |
31 | class BaseMatchForm(forms.Form):
32 | """Base class for match forms.
33 |
34 | Supports converting the form to a match tuple.
35 |
36 | """
37 | _condition_for = None
38 |
39 | def __init__(self, *args, **kwargs):
40 | self._condition_for = kwargs.pop("condition_for", self._condition_for)
41 | super(BaseMatchForm, self).__init__(*args, **kwargs)
42 |
43 | def field_to_mtuple(self):
44 | """Convert the form to a match tuple."""
45 | if not hasattr(self, "cleaned_data"):
46 | if not self.is_valid():
47 | # FIXME
48 | raise Exception("FORM IS NOT VALID: %s" % self.errors)
49 |
50 | field = self._condition_for
51 | return (field,
52 | self.cleaned_data["%s_cond" % field],
53 | self.cleaned_data["%s_query" % field])
54 |
55 | def save(self):
56 | return self.field_to_mtuple()
57 |
58 |
59 | def create_match_forms(name, provides_args):
60 | """With a list of supported arguments, generate a list of match
61 | forms for these.
62 |
63 | E.g. if the supported arguments is ``["name", "address"]``, it will
64 | generate forms like these::
65 |
66 | Name: SELECT:[any|exact|starts with|ends with|contains] [ query ]
67 | Address: SELECT:[any|exact|starts with|ends with|contains] [ query ]
68 |
69 | When these form are feeded with data they can be converted to a match
70 | dict like:
71 |
72 | >>> {"name": Startswith("foo"), "address": Endswith("New York")}
73 |
74 | """
75 |
76 | def gen_field_for_name(name):
77 | return {"%s_cond" % name: forms.ChoiceField(label=name,
78 | choices=MATCHABLE_CHOICES,
79 | widget=forms.Select()),
80 | "%s_query" % name: forms.CharField(label="",
81 | required=False,
82 | initial=""),
83 | }
84 |
85 | def gen_form_for_field(field):
86 | dict_ = gen_field_for_name(field)
87 | dict_["_condition_for"] = field
88 | return type(name + field, (BaseMatchForm, ), dict_)
89 |
90 | return dict((field, gen_form_for_field(field))
91 | for field in provides_args)
92 |
--------------------------------------------------------------------------------
/durian/match/__init__.py:
--------------------------------------------------------------------------------
1 | from durian.match.able import Any, Is, Startswith, Endswith, Contains, Like
2 | from durian.match.strategy import deepmatch
3 | from django.utils.translation import ugettext_lazy as _
4 |
5 | CONDITION_PASS = 0x0
6 | CONDITION_EXACT = 0x1
7 | CONDITION_STARTSWITH = 0x2
8 | CONDITION_ENDSWITH = 0x3
9 | CONDITION_CONTAINS = 0x4
10 |
11 | MATCHABLE_CHOICES = (
12 | (CONDITION_PASS, (_("anything"))),
13 | (CONDITION_EXACT, (_("exact"))),
14 | (CONDITION_STARTSWITH, (_("starts with"))),
15 | (CONDITION_ENDSWITH, (_("ends with"))),
16 | (CONDITION_CONTAINS, (_("contains"))),
17 | )
18 |
19 | CONST_TO_MATCHABLE = {
20 | CONDITION_PASS: Any,
21 | CONDITION_EXACT: Is,
22 | CONDITION_STARTSWITH: Startswith,
23 | CONDITION_ENDSWITH: Endswith,
24 | CONDITION_CONTAINS: Contains,
25 | }
26 |
27 |
28 | def const_to_matchable(const_kind, what):
29 | return CONST_TO_MATCHABLE[int(const_kind)](what)
30 |
31 |
32 | def mtuplelist_to_matchdict(mtuplelist):
33 | """Converts a list of ``(name, kind, what)`` tuples to a match dict.
34 | Where name is the field to match, kind is a matchable number mapping to
35 | ``CONST_TO_MATCHABLE``.
36 |
37 | Possible types are: ``CONDITION_EXACT``, ``CONDITION_STARTSWITH``,
38 | ``CONDITION_ENDSWITH, ``CONDITION_CONTAINS``.
39 |
40 |
41 |
42 | Probably best explained by an example:
43 |
44 | >>> mtuplelist = [("name", CONDITION_ENDSWITH, "Constanza"),
45 | ... ("zipcode", CONDITION_STARTSWITH, "70")]
46 | >>> mtuplelist_to_matchdict(mtuplelist)
47 | {"name": Endswith("Constanza"), "zipcode": Startswith("70")}
48 |
49 | """
50 | return dict((name, const_to_matchable(kind, what) or what)
51 | for name, kind, what in mtuplelist
52 | if int(kind) != CONDITION_PASS)
53 |
--------------------------------------------------------------------------------
/durian/match/able.py:
--------------------------------------------------------------------------------
1 | import re
2 | import operator
3 |
4 |
5 | class Matchable(object):
6 | """Base matchable class.
7 |
8 | :param value: See :attr:`value`.
9 |
10 | Matchables are used to modify the way a value is tested for equality.
11 |
12 | Subclasses of :class:`Matchable` must implement the :meth:`__eq__` method.
13 |
14 | .. attribute:: value
15 |
16 | The value to match against.
17 |
18 | """
19 |
20 | def __init__(self, value):
21 | self.value = value
22 |
23 | def __ne__(self, other):
24 | return not self.__eq__(other)
25 |
26 | def __eq__(self, other):
27 | raise NotImplementedError("Matchable objects must define __eq__")
28 |
29 | def __repr__(self):
30 | return '%s("%s")' % (self.__class__.__name__, self.value)
31 |
32 |
33 | class Any(Matchable):
34 | """Matchable always matching anything."""
35 |
36 | def __init__(self, value):
37 | value = value or ""
38 | super(Any, self).__init__(value)
39 |
40 | def __eq__(self, other):
41 | return True
42 |
43 |
44 | class Is(Matchable):
45 | """Matchable checking for strict equality.
46 |
47 | That is, the values must be identical.
48 | (same as the regular ``==`` operator.)
49 |
50 | """
51 |
52 | def __eq__(self, other):
53 | return operator.eq(self.value, other)
54 |
55 |
56 | class Startswith(Matchable):
57 | """Matchable checking if the matched string starts with the matchee.
58 |
59 | Same as ``other.startswith(value)``.
60 |
61 | """
62 |
63 | def __eq__(self, other):
64 | return other.startswith(self.value)
65 |
66 |
67 | class Endswith(Matchable):
68 | """Matchable checking if the matched string ends with the matchee.
69 |
70 | Same as ``other.endswith(value)``.
71 |
72 | """
73 |
74 | def __eq__(self, other):
75 | return other.endswith(self.value)
76 |
77 |
78 | class Contains(Matchable):
79 | """Matchable checking if the matched string contains the matchee.
80 |
81 | Same as ``other.find(value)``.
82 |
83 | """
84 |
85 | def __eq__(self, other):
86 | return other.find(self.value) != -1
87 |
88 |
89 | class Like(Matchable):
90 | """Matchable checking if the matched string matches a regular
91 | expression."""
92 |
93 | def __init__(self, value):
94 | super(Like, self).__init__(value)
95 | self.pattern = re.compile(self.value)
96 |
97 | def __eq__(self, other):
98 | return bool(self.pattern.search(other))
99 |
--------------------------------------------------------------------------------
/durian/match/strategy.py:
--------------------------------------------------------------------------------
1 | from collections import deque
2 |
3 |
4 | def deepmatch(needle, haystack):
5 | """With a needle dictionary, recursively match all its keys to
6 | the haystack dictionary."""
7 | stream = deque([(needle, haystack)])
8 |
9 | while True:
10 | atom_left, atom_right = stream.pop()
11 | for key, value in atom_left.items():
12 | if isinstance(value, dict):
13 | if key not in atom_right:
14 | return False
15 | stream.append((value, atom_right[key]))
16 | else:
17 | if atom_right.get(key) != value:
18 | return False
19 | if not stream:
20 | break
21 | return True
22 |
--------------------------------------------------------------------------------
/durian/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.utils.translation import ugettext_lazy as _
3 | from celery.fields import PickledObjectField
4 | from celery.serialization import pickle
5 |
6 |
7 | class Listener(models.Model):
8 | hook = models.CharField(_("hook"), max_length=255,
9 | help_text=_("Connects to hook"))
10 | url = models.URLField(verify_exists=False,
11 | help_text=_("The URL I'm listening at."))
12 | created_at = models.DateTimeField(auto_now_add=True)
13 | updated_at = models.DateTimeField(auto_now=True)
14 | config = PickledObjectField(_("configuration"), default=pickle.dumps({}),
15 | help_text=_("Hook specific configuration."))
16 | match = PickledObjectField(_(u"conditions"), default=pickle.dumps({}),
17 | help_text=_("Hook specific event filter"))
18 |
19 | class Meta:
20 | verbose_name = _("listener")
21 | verbose_name_plural = _("listeners")
22 |
23 | def __unicode__(self):
24 | return "%s match:%s config:%s" % (
25 | self.url, self.match, self.config)
26 |
--------------------------------------------------------------------------------
/durian/registry.py:
--------------------------------------------------------------------------------
1 | """durian.registry"""
2 | from celery.exceptions import NotRegistered, AlreadyRegistered
3 | from UserDict import UserDict
4 | from inspect import isclass
5 |
6 |
7 | class HookRegistry(UserDict):
8 | """Global hook registry."""
9 |
10 | AlreadyRegistered = AlreadyRegistered
11 | NotRegistered = NotRegistered
12 |
13 | def __init__(self):
14 | self.data = {}
15 |
16 | def register(self, hook):
17 | """Register a hook in the hook registry.
18 |
19 | :param hook: The hook to register.
20 |
21 | :raises AlreadyRegistered: if the task is already registered.
22 |
23 | """
24 | # instantiate class if not already.
25 | hook = hook() if isclass(hook) else hook
26 |
27 | name = hook.name
28 | if name in self.data:
29 | raise self.AlreadyRegistered(
30 | "Hook with name %s is already registered." % name)
31 |
32 | self.data[name] = hook
33 |
34 | def unregister(self, name):
35 | """Unregister hook by name.
36 |
37 | :param name: name of the hook to unregister, or a
38 | :class:`durian.event.Hook` class with a valid ``name`` attribute.
39 |
40 | :raises celery.exceptions.NotRegistered: if the hook has not
41 | been registered.
42 |
43 | """
44 | if hasattr(name, "name"):
45 | name = name.name
46 | if name not in self.data:
47 | raise self.NotRegistered(
48 | "Hook with name %s is not registered." % name)
49 | del self.data[name]
50 |
51 | def get_all(self):
52 | """Get all hooks."""
53 | return self.data
54 |
55 | def get_hook(self, name):
56 | """Get hook by name."""
57 | return self.data[name]
58 |
59 | def as_choices(self):
60 | """Return the hook registry as a choices tuple for use
61 | within Django models and forms."""
62 | dict_types = dict((type.name, type)
63 | for type in self.data.values())
64 | sorted_names = sorted(dict_types.keys())
65 | return [(type.name, type.verbose_name.capitalize())
66 | for name, type in dict_types.items()]
67 |
68 | """
69 | .. data:: hooks
70 |
71 | The global hook registry.
72 |
73 | """
74 | hooks = HookRegistry()
75 |
--------------------------------------------------------------------------------
/durian/tasks.py:
--------------------------------------------------------------------------------
1 | from celery.task.base import Task
2 | from celery.registry import tasks
3 | from celery.exceptions import MaxRetriesExceededError
4 | from anyjson import serialize
5 |
6 |
7 | class WebhookSignal(Task):
8 | """The default web hook action. Simply sends the payload to the
9 | listener URL as POST data.
10 |
11 | Task arguments
12 |
13 | * url
14 | The listener destination URL to send payload to.
15 |
16 | * payload
17 | The payload to send to the listener.
18 |
19 | """
20 | name = "durian.tasks.WebhookSignal"
21 | ignore_result = True
22 |
23 | def run(self, url, payload, **kwargs):
24 | import urllib2
25 | import socket
26 |
27 | orig_timeout = socket.getdefaulttimeout()
28 | retry = kwargs.get("retry", False)
29 | fail_silently = kwargs.get("fail_silently", False)
30 | self.max_retries = kwargs.get("max_retries", self.max_retries)
31 | timeout = kwargs.get("timeout", orig_timeout)
32 |
33 | socket.setdefaulttimeout(timeout)
34 | try:
35 | urllib2.urlopen(url, serialize(payload))
36 | except urllib2.URLError, exc:
37 | if self.retry:
38 | try:
39 | self.retry(args=[url, payload], kwargs=kwargs, exc=exc)
40 | except MaxRetriesExceededError:
41 | if self.fail_silently:
42 | return
43 | raise
44 | else:
45 | if fail_silently:
46 | return
47 | raise
48 | finally:
49 | socket.setdefaulttimeout(orig_timeout)
50 |
--------------------------------------------------------------------------------
/durian/templates/durian/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block extrastyle %}{% endblock %}
6 |
7 | {% block title %}{% endblock %}
8 |
9 | {% block site_extrahead %}{% endblock %}
10 | {% block extrahead %}{% endblock %}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 | {% block pretitle %}{% endblock %}
29 |
{% block content_title %}{% endblock %}
30 |
31 | {% block objecttools %}{% endblock %}
32 | {% block sidebar %}{% endblock %}
33 | {% block content %}{{ content }}{% endblock %}
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/durian/templates/durian/base_site.html:
--------------------------------------------------------------------------------
1 | {% extends "durian/base.html" %}
2 |
3 | {% block title %}Durian | {{ title }}{% endblock %}
4 |
5 | {% block site_name_header %}
6 | Durian
7 | {% endblock %}
8 |
9 | {% block content_title %}{{ title }}{% endblock %}
10 |
11 | {% block nav-global %}{% endblock %}
12 |
13 |
--------------------------------------------------------------------------------
/durian/templates/durian/create_hook.html:
--------------------------------------------------------------------------------
1 | {% extends "durian/base_site.html" %}
2 | {% load i18n %}
3 |
4 | {% block content %}
5 |
15 | {% endfor %}
16 |
17 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/durian/templates/durian/select_hook.html:
--------------------------------------------------------------------------------
1 | {% extends "durian/base_site.html" %}
2 |
3 | {% block content %}
4 |
5 | Please select the event you want to attach to.
6 |
7 |
8 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/durian/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from djangox.test.depth import alltests
2 |
3 |
4 | def suite():
5 | return alltests(__file__, __name__)
6 |
--------------------------------------------------------------------------------
/durian/tests/test_hooks.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from durian.event import Hook, ModelHook, IntermediateListener
3 | from durian.registry import hooks
4 | from durian.forms import BaseMatchForm
5 | from durian.models import Listener
6 | from celery.registry import tasks
7 | from durian.tasks import WebhookSignal
8 | from durian import match
9 | from django.contrib.auth.models import User
10 | from django.db.models import signals
11 | from django.dispatch import Signal
12 |
13 |
14 | class TestWebhookSignal(WebhookSignal):
15 | name = "__durian__.unittest.TestWebhookSignal"
16 | ignore_result = True
17 | scratchpad = {}
18 |
19 | def run(self, url, payload, **kwargs):
20 | self.__class__.scratchpad[url] = payload
21 |
22 |
23 | testhook = Hook(name="__durian__.unittest.testhook",
24 | provides_args=["name", "address", "phone", "email"],
25 | task_cls=TestWebhookSignal,
26 | async=False)
27 | hooks.register(testhook)
28 |
29 | testmhook = ModelHook(model=User, signal=signals.post_save,
30 | name="__durian__.unittest.testmhook",
31 | provides_args=["username", "email", "first_name",
32 | "last_name"],
33 | task_cls=TestWebhookSignal,
34 | async=False)
35 | hooks.register(testmhook)
36 |
37 |
38 | class TestHook(unittest.TestCase):
39 |
40 | def test_in_registry(self):
41 | in_reg = hooks.get("__durian__.unittest.testhook")
42 | self.assertTrue(in_reg)
43 | self.assertTrue(isinstance(in_reg, Hook))
44 |
45 | def test_match_forms(self):
46 | mforms = testhook.match_forms
47 | for field in testhook.provides_args:
48 | self.assertTrue(field in mforms)
49 | mform = mforms[field]()
50 | self.assertTrue(isinstance(mform, BaseMatchForm))
51 | self.assertTrue("%s_cond" % field in mform.base_fields)
52 | self.assertTrue("%s_query" % field in mform.base_fields)
53 |
54 | matchdict = testhook.apply_match_forms({
55 | "name_cond": match.CONDITION_EXACT,
56 | "name_query": "George Constanza",
57 | "address_cond": match.CONDITION_ENDSWITH,
58 | "address_query": "New York City",
59 | "phone_cond": match.CONDITION_STARTSWITH,
60 | "phone_query": "212",
61 | "email_cond": match.CONDITION_CONTAINS,
62 | "email_query": "@vandelay"})
63 |
64 | self.assertTrue(isinstance(matchdict.get("name"), match.Is))
65 | self.assertTrue(isinstance(matchdict.get("address"), match.Endswith))
66 | self.assertTrue(isinstance(matchdict.get("phone"), match.Startswith))
67 | self.assertTrue(isinstance(matchdict.get("email"), match.Contains))
68 |
69 | self.assertTrue(match.deepmatch(matchdict, {
70 | "name": "George Constanza",
71 | "address": "Border Lane, New York City",
72 | "phone": "212 555 88 23",
73 | "email": "george@vandelay.com",
74 | }))
75 |
76 | def test_trigger_event(self):
77 | url = "http://where.joe/listens"
78 | form = testhook.config_form({"url": url})
79 | listener = testhook.listener(form).match(name="Joe").save()
80 | self.assertTrue(isinstance(listener, Listener))
81 |
82 | lis = Listener.objects.filter(url=url)
83 | self.assertTrue(lis.count())
84 | self.assertTrue(lis[0].url == url)
85 |
86 | testhook.send(sender=self,
87 | name="Joe", address="foo", phone="123",
88 | email="joe@example.com")
89 |
90 | self.assertTrue(TestWebhookSignal.scratchpad.get(url))
91 | del(TestWebhookSignal.scratchpad[url])
92 |
93 | testhook.send(sender=self,
94 | name="Simon", address="bar", phone="456",
95 | email="simon@example.com")
96 | self.assertFalse(TestWebhookSignal.scratchpad.get(url))
97 |
98 |
99 | class TestModelHook(unittest.TestCase):
100 |
101 | def test_trigger_event(self):
102 | url = "http://where.joe/mlistens"
103 | form = testmhook.config_form({"url": url})
104 | self.assertTrue(testmhook)
105 | a = testmhook.listener(form)
106 | self.assertTrue(isinstance(a, IntermediateListener))
107 | self.assertTrue(callable(a.match))
108 | self.assertTrue(testmhook.listener(form).match(username="joe"))
109 | listener = testmhook.listener(form).match(username="joe").save()
110 | self.assertTrue(isinstance(listener, Listener))
111 |
112 | lis = Listener.objects.filter(url=url)
113 | self.assertTrue(lis.count())
114 | self.assertTrue(lis[0].url == url)
115 |
116 | u = User.objects.create_user(username="joe",
117 | email="joe@example.com", password="joe")
118 |
119 | scratch = TestWebhookSignal.scratchpad.get(url)
120 | self.assertTrue(scratch)
121 | self.assertTrue(scratch["created"])
122 | self.assertEquals(scratch["username"], "joe")
123 | self.assertEquals(scratch["email"], "joe@example.com")
124 | self.assertFalse(scratch.get("password"))
125 | del(TestWebhookSignal.scratchpad[url])
126 |
127 | u.last_name = "Example"
128 | u.save()
129 |
130 | scratch = TestWebhookSignal.scratchpad.get(url)
131 | self.assertTrue(scratch)
132 | self.assertEquals(scratch["created"], False)
133 | self.assertEquals(scratch["username"], "joe")
134 | self.assertEquals(scratch["email"], "joe@example.com")
135 | self.assertEquals(scratch["last_name"], "Example")
136 | self.assertFalse(scratch.get("password"))
137 | del(TestWebhookSignal.scratchpad[url])
138 |
--------------------------------------------------------------------------------
/durian/tests/test_match.py:
--------------------------------------------------------------------------------
1 | from durian.match.strategy import deepmatch
2 | from durian.match.able import Is, Like, Startswith, Endswith, Contains
3 | from durian.match import mtuplelist_to_matchdict
4 | from durian import match
5 | import unittest
6 |
7 |
8 | class TestMtuplelist(unittest.TestCase):
9 |
10 | def test_conversion(self):
11 | mtuplelist = [("name", match.CONDITION_ENDSWITH, "Constanza"),
12 | ("zipcode", match.CONDITION_STARTSWITH, "70"),
13 | ("address", match.CONDITION_EXACT, "Milkyway"),
14 | ("work", match.CONDITION_CONTAINS, "andeley"),
15 | ("zoo", match.CONDITION_PASS, "zoo")]
16 | matchdict = mtuplelist_to_matchdict(mtuplelist)
17 | self.assertFalse(matchdict.get("zoo"))
18 | self.assertTrue(isinstance(matchdict.get("name"), Endswith))
19 | self.assertEquals(matchdict["name"].value, "Constanza")
20 | self.assertTrue(isinstance(matchdict.get("zipcode"), Startswith))
21 | self.assertEquals(matchdict["zipcode"].value, "70")
22 | self.assertTrue(isinstance(matchdict.get("address"), Is))
23 | self.assertEquals(matchdict["address"].value, "Milkyway")
24 | self.assertTrue(isinstance(matchdict.get("work"), Contains))
25 | self.assertEquals(matchdict["work"].value, "andeley")
26 |
27 | matches = {"name": "George Constanza",
28 | "zipcode": "70312",
29 | "address": "Milkyway",
30 | "work": "Vandeley Industries"}
31 |
32 | notmatches = {"name": "Jerry Seinfeld",
33 | "zipcode": "70314",
34 | "address": "Milkyway",
35 | "work": "Comedian"}
36 |
37 | self.assertTrue(deepmatch(matches, matchdict))
38 | self.assertFalse(deepmatch(notmatches, matchdict))
39 |
40 |
41 | class TestStrategy(unittest.TestCase):
42 |
43 | def test_deepmatch(self):
44 | self.assertTrue(deepmatch({"name": Like("Constanza")},
45 | {"name": "George Constanza"}))
46 | self.assertFalse(deepmatch({"name": Like("Seinfeld")},
47 | {"name": "George Constanza"}))
48 | self.assertTrue(deepmatch({"name": Startswith("George")},
49 | {"name": "George Constanza"}))
50 | self.assertFalse(deepmatch({"name": Startswith("Cosmo")},
51 | {"name": "George Constanza"}))
52 | self.assertTrue(deepmatch({"name": {
53 | "first_name": Startswith("El"),
54 | "last_name": Endswith("es"),
55 | }},
56 | {"name": {
57 | "first_name": "Elaine",
58 | "last_name": "Benes",
59 | }}))
60 | x = {
61 | "foo": "xuzzy",
62 | "baz": "xxx",
63 | "mooze": {
64 | "a": "b",
65 | "c": {
66 | "d": "e",
67 | }
68 | }
69 | }
70 | self.assertTrue(deepmatch(x, x))
71 | self.assertFalse(deepmatch(x, {"foo": "xuzzy",
72 | "baz": "xxx",
73 | "mooze": {
74 | "a": "b",
75 | "c": {
76 | "x": "y",
77 | }
78 | }
79 | }))
80 | self.assertTrue(deepmatch({"foo": "bar"}, {"foo": "bar"}))
81 | self.assertFalse(deepmatch({"foo": 1}, {"foo": "1"}))
82 | self.assertFalse(deepmatch({"foo": "bar", "baz": {"x": "x"}},
83 | {"foo": "bar"}))
84 |
--------------------------------------------------------------------------------
/durian/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import patterns, url, include
2 | from durian import views
3 |
4 |
5 | class DurianSite(object):
6 | namespace = "durian"
7 | app_name = "durian"
8 |
9 | def __init__(self, namespace=None, app_name=None):
10 | self.namespace = namespace or self.namespace
11 | self.app_name = app_name or self.app_name
12 |
13 | @property
14 | def urlpatterns(self):
15 | return patterns("",
16 | url(r'^select/', views.select, name="select"),
17 | url(r'^create/', views.create, name="create"),
18 | url(r'^debug/', views.debug, name="debug"),
19 | )
20 |
21 | @property
22 | def urls(self):
23 | return (self.urlpatterns, "durian", "durian")
24 |
25 |
26 | durian = DurianSite()
27 | urlpatterns = durian.urlpatterns
28 |
--------------------------------------------------------------------------------
/durian/views.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from django.shortcuts import render_to_response
3 | from django.template import RequestContext
4 | from django.http import HttpResponse, HttpResponseRedirect
5 | from django.http import HttpResponseNotAllowed
6 | from django.utils.translation import ugettext as _
7 | from anyjson import deserialize
8 | from durian.forms import create_select_hook_form
9 | from durian.registry import hooks
10 |
11 |
12 | def send(request, hook_type):
13 | """Trigger hook by sending payload as POST data."""
14 |
15 | if request.method != "POST":
16 | return HttpResponseNotAllowed(_("Method not allowed: %s") % (
17 | request.method))
18 |
19 | payload = dict(request.POST.copy())
20 | sender = request.META
21 |
22 | hook = get_hook_or_404(hook_type)
23 | hook.send(sender=sender, **payload)
24 |
25 |
26 | def select(request, template_name="durian/select_hook.html"):
27 | """Select hook to create."""
28 | context = RequestContext(request)
29 | context["title"] = _("Select event")
30 | context["select_hook_form"] = create_select_hook_form()
31 | return render_to_response(template_name, context_instance=context)
32 |
33 |
34 | def get_hook_or_404(hook_type):
35 | """Get hook by type in the registry or raise HTTP 404."""
36 | if hook_type not in hooks:
37 | raise Http404(_("Unknown hook type: %s" % hook_type))
38 | return hooks[hook_type]
39 |
40 |
41 | def create(request, template_name="durian/create_hook.html"):
42 | """View to create a new hook."""
43 | context = RequestContext(request)
44 | if request.method == "POST":
45 | hook = get_hook_or_404(request.POST["type"])
46 | config_form = hook.config_form(request.POST)
47 | if config_form.is_valid():
48 | matchdict = hook.apply_match_forms(request.POST)
49 | hook.add_listener_by_form(config_form, match=matchdict)
50 | return HttpResponse("Listener Created!")
51 | else:
52 | hook = get_hook_or_404(request.GET["type"])
53 | config_form = hook.config_form()
54 |
55 | match_forms = hook.get_match_forms()
56 | context["title"] = _("Create %s Listener" % (
57 | hook.verbose_name.capitalize()))
58 | context["hook_type"] = hook.name
59 | context["hook_name"] = hook.verbose_name
60 | context["match_forms"] = match_forms
61 | context["config_form"] = config_form
62 | return render_to_response(template_name, context_instance=context)
63 |
64 |
65 | def debug(request):
66 | """Simple listener destination URL to dump out the payload and
67 | request to stderr."""
68 | sys.stderr.write(str(request.get_full_path()) + "\n")
69 | sys.stderr.write(str(request.raw_post_data) + "\n")
70 | sys.stderr.write(str(request.POST) + "\n")
71 | sys.stderr.write(str(deserialize(request.raw_post_data)) + "\n")
72 | return HttpResponse("Thanks!")
73 |
--------------------------------------------------------------------------------
/examples/durianproject/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ask/durian/bd7a773ce3470884fd74e97f0bba4b351545df56/examples/durianproject/__init__.py
--------------------------------------------------------------------------------
/examples/durianproject/durianapp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ask/durian/bd7a773ce3470884fd74e97f0bba4b351545df56/examples/durianproject/durianapp/__init__.py
--------------------------------------------------------------------------------
/examples/durianproject/durianapp/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.db.models import signals
3 | from django.utils.translation import ugettext_lazy as _
4 | from durian.event import ModelHook
5 | from durian.registry import hooks
6 |
7 |
8 | class Person(models.Model):
9 | name = models.CharField(_(u"name"), blank=False, max_length=200)
10 | address = models.CharField(_(u"address"), max_length=200)
11 | secret = models.CharField(_(u"secret"), max_length=200)
12 |
13 |
14 | class PersonHook(ModelHook):
15 | name = "person"
16 | model = Person
17 | signal = signals.post_save
18 | provides_args = ["name", "address"]
19 | async = False
20 | hooks.register(PersonHook)
21 |
--------------------------------------------------------------------------------
/examples/durianproject/durianapp/tests.py:
--------------------------------------------------------------------------------
1 | """
2 | This file demonstrates two different styles of tests (one doctest and one
3 | unittest). These will both pass when you run "manage.py test".
4 |
5 | Replace these with more appropriate tests for your application.
6 | """
7 |
8 | from django.test import TestCase
9 |
10 |
11 | class SimpleTest(TestCase):
12 |
13 | def test_basic_addition(self):
14 | """Tests that 1 + 1 always equals 2."""
15 | self.failUnlessEqual(1 + 1, 2)
16 |
17 | __test__ = {"doctest": """
18 | Another way to test that 1 + 1 is equal to 2.
19 |
20 | >>> 1 + 1 == 2
21 | True
22 | """}
23 |
--------------------------------------------------------------------------------
/examples/durianproject/durianapp/views.py:
--------------------------------------------------------------------------------
1 | # Create your views here.
2 |
--------------------------------------------------------------------------------
/examples/durianproject/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from django.core.management import execute_manager
3 | try:
4 | import settings # Assumed to be in the same directory.
5 | except ImportError:
6 | import sys
7 | sys.stderr.write(
8 | "Error: Can't find the file 'settings.py' in the " + \
9 | "directory containing %r. It appears you've" % __file__ + \
10 | "customized things.\nYou'll have to run " + \
11 | "django-admin.py, passing it your settings module.\n" + \
12 | "(If the file settings.py does indeed exist, it's " + \
13 | "causing an ImportError somehow.)\n")
14 | sys.exit(1)
15 |
16 | if __name__ == "__main__":
17 | execute_manager(settings)
18 |
--------------------------------------------------------------------------------
/examples/durianproject/settings.py:
--------------------------------------------------------------------------------
1 | # Django settings for durianproject project.
2 |
3 | DEBUG = True
4 | TEMPLATE_DEBUG = DEBUG
5 |
6 | ADMINS = (
7 | # ('Your Name', 'your_email@domain.com'),
8 | )
9 |
10 | MANAGERS = ADMINS
11 |
12 | DATABASE_ENGINE = 'sqlite3'
13 | DATABASE_NAME = 'durianproject.sqlite'
14 | DATABASE_USER = ''
15 | DATABASE_PASSWORD = ''
16 | DATABASE_HOST = ''
17 | DATABASE_PORT = ''
18 |
19 | # Local time zone for this installation. Choices can be found here:
20 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
21 | # although not all choices may be available on all operating systems.
22 | # If running in a Windows environment this must be set to the same as your
23 | # system time zone.
24 | TIME_ZONE = 'America/Chicago'
25 |
26 | # Language code for this installation. All choices can be found here:
27 | # http://www.i18nguy.com/unicode/language-identifiers.html
28 | LANGUAGE_CODE = 'en-us'
29 |
30 | SITE_ID = 1
31 |
32 | # If you set this to False, Django will make some optimizations so as not
33 | # to load the internationalization machinery.
34 | USE_I18N = True
35 |
36 | # Absolute path to the directory that holds media.
37 | # Example: "/home/media/media.lawrence.com/"
38 | MEDIA_ROOT = ''
39 |
40 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a
41 | # trailing slash if there is a path component (optional in other cases).
42 | # Examples: "http://media.lawrence.com", "http://example.com/media/"
43 | MEDIA_URL = ''
44 |
45 | # URL prefix for admin media -- CSS, JavaScript and images.
46 | # Make sure to use a trailing slash.
47 | # Examples: "http://foo.com/media/", "/media/".
48 | ADMIN_MEDIA_PREFIX = '/media/'
49 |
50 | # Make this unique, and don't share it with anybody.
51 | SECRET_KEY = '+s)c0psuin+78$f2(k7)bxglr-i+_wy*lz)))48m@_1(zgogho'
52 |
53 | # List of callables that know how to import templates from various sources.
54 | TEMPLATE_LOADERS = (
55 | 'django.template.loaders.filesystem.load_template_source',
56 | 'django.template.loaders.app_directories.load_template_source',
57 | )
58 |
59 | MIDDLEWARE_CLASSES = (
60 | 'django.middleware.common.CommonMiddleware',
61 | 'django.contrib.sessions.middleware.SessionMiddleware',
62 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
63 | )
64 |
65 | ROOT_URLCONF = 'durianproject.urls'
66 |
67 | INSTALLED_APPS = (
68 | 'django.contrib.auth',
69 | 'django.contrib.contenttypes',
70 | 'django.contrib.sessions',
71 | 'django.contrib.sites',
72 | 'django.contrib.admin',
73 | 'celery',
74 | 'durian',
75 | 'durianapp',
76 | )
77 |
--------------------------------------------------------------------------------
/examples/durianproject/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import patterns, url, include
2 | from durian.urls import durian
3 | from django.contrib import admin
4 | from django.conf import settings
5 | admin.autodiscover()
6 |
7 | # Uncomment the next two lines to enable the admin:
8 | # from django.contrib import admin
9 | # admin.autodiscover()
10 |
11 | urlpatterns = patterns('',
12 | url(r'^durian/', durian.urls),
13 | url(r'^admin/', admin.site.urls),
14 | url(r"%s(?P.*)$" % settings.MEDIA_URL[1:],
15 | "django.views.static.serve", {
16 | "document_root": settings.MEDIA_ROOT}),
17 | )
18 |
--------------------------------------------------------------------------------
/examples/hook.py:
--------------------------------------------------------------------------------
1 | from durian.models import Listener
2 | from django import forms
3 | from durian.forms import HookConfigForm
4 | from durian.event import Hook
5 | from durian.registry import hooks
6 |
7 |
8 | # SIMPLE HOOK
9 |
10 | class MyHook(Hook):
11 | name = "myhook"
12 | hooks.register(MyHook())
13 |
14 |
15 | def install_listener():
16 | l = Listener(url="http://localhost:8000/durian/debug/",
17 | hook=MyHook.name)
18 | l.save()
19 |
20 | # HOOK SENT EVERY TIME USER "ask" COMMITS A CHANGE
21 |
22 | # This form is needed to add a listener, so the correct username/password
23 | # is sent when sending updates to twitter.
24 |
25 | class TwitterCommitHookConfigForm(HookConfigForm):
26 | username = forms.CharField(label=_(u"username"))
27 | password = forms.CharField(label=_(u"password"), required=True,
28 | widget=forms.PasswordInput())
29 | digest = forms.BooleanField(widget=forms.CheckboxInput())
30 | active = forms.BooleanField(widget=forms.CheckboxInput())
31 |
32 |
33 | # This is the hook itself.
34 | class TwitterCommitHook(Hook):
35 | name = "myuserhook"
36 | config_form = TwitterCommitHookConfigForm
37 | providing_args = ["username", "password", "digest", "active"]
38 | hooks.register(TwitterCommitHook)
39 |
40 |
41 | # This is the function triggering the hook
42 | def commit_change(self, commit_msg, user, revision):
43 |
44 | # ...Do what happens regularly at a commit...
45 |
46 | TwitterCommitHook().send(sender=commit_change, user=user, revision=revision,
47 | commit_msg=commit_msg)
48 |
49 |
50 |
51 | # Now let's register a listener.
52 |
53 | from celeryhook.match import Startswith
54 | hook = TwitterCommitHook()
55 | form = hook.config_form({"username": "ask", "password": "foo"})
56 | hook.listener(form).match(commit_msg=Startswith("Important change"),
57 | username="ask").save()
58 |
59 |
60 |
61 |
62 |
63 |
64 | # A Django view registering a listener.
65 | def add_twitter_hook(request, template_name="myapp/twitterhook.html"):
66 | hook = TwitterCommitHook()
67 | context = RequestContext()
68 | if request.method == "POST":
69 | form = hook.config_form(request.POST)
70 | if form.is_valid():
71 | hook.add_listener_by_form(form)
72 | else:
73 | form = hook.config_form()
74 |
75 | context["form"] = form
76 |
77 | return render_to_response(template_name, context_instance=context)
78 |
79 |
80 | # ### MODEL HOOK
81 |
82 |
83 | from django.db import signals
84 | from django.contrib.auth.models import User
85 | from durian.event import ModelHook
86 |
87 |
88 | userhook = ModelHook(name="user-post-save",
89 | model=User,
90 | signal=signals.post_save,
91 | provides_args=["username", "is_admin"])
92 |
93 | # send event when Joe is changed
94 | userhook.listener(
95 | url="http://where.joe/is/listening").match(
96 | username="joe").save()
97 |
98 | # send event when any user is changed.
99 | userhook.listener(url="http://where.joe/is/listening").save()
100 |
101 | # Send event when Joe is admin
102 | userhook.listener(
103 | url="http://where.joe/is/listening").match(
104 | username="joe", is_admin=True).save()
105 |
106 |
107 |
108 |
109 | joe = User.objects.get(username="joe")
110 | joe.is_admin = True
111 | joe.save()
112 |
113 |
114 | A hook that sends events to twitter
115 | -----------------------------------
116 |
117 |
118 | In ``myapp/tasks.py``:
119 |
120 | >>> from celery.task import Task
121 | >>> from celery.registry import tasks
122 |
123 | >>> class TwitterUpdateTask(WebhookSignal):
124 | ... name = "myapp.tasks.TwitterWebhookSignal"
125 | ...
126 | ... def run(self, username, password, message, \*\*kwargs):
127 | ... import twitter
128 | ... api = twitter.Api(username=username, password=password)
129 | ... api.PostUpdate(message)
130 | >>> tasks.register(TwitterUpdateTask)
131 |
132 |
133 | In ``myapp/hooks.py``:
134 |
135 | >>> from durian.event import Hook
136 | >>> from durian.registry import hooks
137 | >>> from durian.forms import BaseHookConfigForm
138 | >>> from django.utils.translation import _
139 | >>> from django import forms
140 |
141 | >>> class TwitterHookConfigForm(HookConfigForm):
142 | ... username = forms.CharField(label=_("twitter username"),
143 | ... required=True)
144 | ... password = forms.CharField(label=_("twitter password"),
145 | ... widget=forms.PasswordInput())
146 |
147 |
148 | >>> class TwitterHook(Hook):
149 | ... name = "Twitter"
150 | ... task_cls = TwitterUpdateTask
151 | ... config_form = TwitterHookConfigForm
152 | ...
153 | ... def
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [nosetests]
2 | verbosity = 1
3 | detailed-errors = 1
4 | with-coverage = 1
5 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import codecs
4 | import sys
5 | import os
6 |
7 | try:
8 | from setuptools import setup, find_packages, Command
9 | except ImportError:
10 | from ez_setup import use_setuptools
11 | use_setuptools()
12 | from setuptools import setup, find_packages, Command
13 |
14 | import durian
15 |
16 |
17 | class RunTests(Command):
18 | description = "Run the django test suite from the testproj dir."
19 |
20 | user_options = []
21 |
22 | def initialize_options(self):
23 | pass
24 |
25 | def finalize_options(self):
26 | pass
27 |
28 | def run(self):
29 | this_dir = os.getcwd()
30 | testproj_dir = os.path.join(this_dir, "testproj")
31 | os.chdir(testproj_dir)
32 | sys.path.append(testproj_dir)
33 | from django.core.management import execute_manager
34 | os.environ["DJANGO_SETTINGS_MODULE"] = os.environ.get(
35 | "DJANGO_SETTINGS_MODULE", "settings")
36 | settings_file = os.environ["DJANGO_SETTINGS_MODULE"]
37 | settings_mod = __import__(settings_file, {}, {}, [''])
38 | execute_manager(settings_mod, argv=[
39 | __file__, "test"])
40 | os.chdir(this_dir)
41 |
42 |
43 | install_requires = ["django-unittest-depth",
44 | "celery"]
45 |
46 | if os.path.exists("README.rst"):
47 | long_description = codecs.open("README.rst", "r", "utf-8").read()
48 | else:
49 | long_description = "See http://pypi.python.org/pypi/celery"
50 |
51 |
52 | setup(
53 | name='durian',
54 | version=durian.__version__,
55 | description=durian.__doc__,
56 | author=durian.__author__,
57 | author_email=durian.__contact__,
58 | url=durian.__homepage__,
59 | platforms=["any"],
60 | packages=find_packages(exclude=['ez_setup']),
61 | zip_safe=False,
62 | install_requires=install_requires,
63 | cmdclass = {"test": RunTests},
64 | classifiers=[
65 | "Development Status :: 4 - Beta",
66 | "Framework :: Django",
67 | "Environment :: Web Environment",
68 | "Topic :: Internet :: WWW/HTTP",
69 | "Topic :: Communications",
70 | "Intended Audience :: Developers",
71 | "Programming Language :: Python",
72 | "Operating System :: OS Independent",
73 | "License :: OSI Approved :: BSD License",
74 | ],
75 | long_description=long_description,
76 | )
77 |
--------------------------------------------------------------------------------
/testproj/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ask/durian/bd7a773ce3470884fd74e97f0bba4b351545df56/testproj/__init__.py
--------------------------------------------------------------------------------
/testproj/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from django.core.management import execute_manager
3 | try:
4 | import settings # Assumed to be in the same directory.
5 | except ImportError:
6 | import sys
7 | sys.stderr.write(
8 | "Error: Can't find the file 'settings.py' in the directory \
9 | containing %r. It appears you've customized things.\n\
10 | You'll have to run django-admin.py, passing it your settings\
11 | module.\n(If the file settings.py does indeed exist, it's\
12 | causing an ImportError somehow.)\n" % __file__)
13 | sys.exit(1)
14 |
15 | if __name__ == "__main__":
16 | execute_manager(settings)
17 |
--------------------------------------------------------------------------------
/testproj/settings.py:
--------------------------------------------------------------------------------
1 | # Django settings for testproj project.
2 |
3 | import os
4 | import sys
5 | # import source code dir
6 | sys.path.insert(0, os.path.join(os.getcwd(), os.pardir))
7 |
8 | SITE_ID = 301
9 |
10 | DEBUG = True
11 | TEMPLATE_DEBUG = DEBUG
12 |
13 | ROOT_URLCONF = "urls"
14 |
15 | ADMINS = (
16 | # ('Your Name', 'your_email@domain.com'),
17 | )
18 |
19 | TEST_RUNNER = "celery.tests.runners.run_tests"
20 | TEST_APPS = (
21 | "durian",
22 | )
23 |
24 | AMQP_SERVER = "localhost"
25 | AMQP_PORT = 5672
26 | AMQP_VHOST = "/"
27 | AMQP_USER = "guest"
28 | AMQP_PASSWORD = "guest"
29 |
30 | TT_HOST = "localhost"
31 | TT_PORT = 1978
32 |
33 | CELERY_AMQP_EXCHANGE = "testdurian"
34 | CELERY_AMQP_ROUTING_KEY = "testdurian"
35 | CELERY_AMQP_CONSUMER_QUEUE = "testdurian"
36 |
37 | MANAGERS = ADMINS
38 |
39 | DATABASE_ENGINE = 'sqlite3'
40 | DATABASE_NAME = 'testdb.sqlite'
41 | DATABASE_USER = ''
42 | DATABASE_PASSWORD = ''
43 | DATABASE_HOST = ''
44 | DATABASE_PORT = ''
45 |
46 | INSTALLED_APPS = (
47 | 'django.contrib.auth',
48 | 'django.contrib.contenttypes',
49 | 'django.contrib.sessions',
50 | 'django.contrib.sites',
51 | 'celery',
52 | 'durian',
53 | )
54 |
55 | try:
56 | import test_extensions
57 | except ImportError:
58 | pass
59 | else:
60 | pass
61 | #INSTALLED_APPS += ("test_extensions", )
62 |
63 | SEND_CELERY_TASK_ERROR_EMAILS = False
64 |
--------------------------------------------------------------------------------
/testproj/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls.defaults import *
2 |
3 | # Uncomment the next two lines to enable the admin:
4 | # from django.contrib import admin
5 | # admin.autodiscover()
6 |
7 | urlpatterns = patterns('',
8 | # Example:
9 | # (r'^testproj/', include('testproj.foo.urls')),
10 |
11 | # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
12 | # to INSTALLED_APPS to enable admin documentation:
13 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
14 |
15 | # Uncomment the next line to enable the admin:
16 | # (r'^admin/(.*)', admin.site.root),
17 | )
18 |
--------------------------------------------------------------------------------