├── .gitignore ├── LICENSE ├── MANIFEST.in ├── NOTICE ├── README.rst ├── doc ├── Makefile ├── ghp-import ├── make.bat ├── source │ ├── _static │ │ └── http-headers-status-v3.png │ ├── auth.rst │ ├── conf.py │ ├── diagram.rst │ ├── docs.rst │ ├── index.rst │ ├── introduction.rst │ ├── quickstart.rst │ ├── recipes.rst │ ├── resources.rst │ ├── sphinxtogithub.py │ ├── throttling.rst │ └── wm.rst └── sphinxtogithub.py ├── example ├── helloworld │ ├── __init__.py │ ├── hello │ │ ├── __init__.py │ │ └── resource.py │ ├── manage.py │ ├── settings.py │ └── urls.py ├── helloworld2 │ ├── __init__.py │ ├── hello │ │ ├── __init__.py │ │ └── resources.py │ ├── manage.py │ ├── settings.py │ └── urls.py └── testoauth │ ├── __init__.py │ ├── manage.py │ ├── protected │ ├── __init__.py │ └── resource.py │ ├── settings.py │ └── urls.py ├── setup.py └── webmachine ├── __init__.py ├── auth ├── __init__.py ├── base.py ├── oauth.py ├── oauth_res.py └── oauth_store.py ├── decisions.py ├── exc.py ├── forms.py ├── helpers ├── __init__.py └── serialize.py ├── managers.py ├── media ├── http-headers-status-v3.png ├── map.png ├── wmtrace.css └── wmtrace.js ├── models.py ├── resource.py ├── route.py ├── templates ├── webmachine │ └── authorize_token.html └── wm │ ├── base.html │ ├── wmtrace.html │ └── wmtrace_list.html ├── throttle.py ├── util ├── __init__.py └── const.py ├── wmtrace.py └── wrappers.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.db 4 | *.swp 5 | *.pyo 6 | .installed.cfg 7 | dj_webmachine.egg-info/ 8 | dist 9 | build 10 | doc/build/doctrees 11 | doc/build/html 12 | examples/testoauth/oauth.db 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Benoit Chesneau 2 | Copyright 2010 (c) Hyperweek - http://hyperweek.net 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include NOTICE 3 | include README.rst 4 | recursive-include webmachine/media * 5 | recursive-include webmachine/templates * 6 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | dj-webmachine 2 | ----------- 3 | 4 | 2010 (c) Benoît Chesneau 5 | 2010 (c) Hyperweek - http://hyperweek.net 6 | 7 | dj-webmachine is released under the MIT license. See the LICENSE 8 | file for the complete license. 9 | 10 | decisions.py 11 | ------------ 12 | Adapted from py-webmachine under MIT license. 13 | Copyright (c) 2009-2010 Paul J. Davis 14 | 15 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | dj-webmachine 2 | ------------- 3 | 4 | dj-webmachine is an application layer that adds HTTP semantic awareness on 5 | top of Django and provides a simple and clean way to connect that to 6 | your applications' behavior. dj-webmachine also offers you the 7 | possibility to build simple API based on your model and the tools to 8 | create automatically docs and clients from it (work in progress). 9 | 10 | dj-webmachine is released under the MIT license. See the LICENSE 11 | file for the complete license. 12 | 13 | Copyright (c) 2010 Benoit Chesneau 14 | Copyright 2010 (c) Hyperweek - http://hyperweek.net 15 | 16 | 17 | Install 18 | +++++++ 19 | 20 | Make sure that you have a working Python_ 2.x >=2.5 installed and Django_ >= 1.1. 21 | 22 | 23 | With pip 24 | ~~~~~~~~ 25 | 26 | :: 27 | 28 | $ pip install dj-webmachine 29 | 30 | From source 31 | ~~~~~~~~~~~ 32 | 33 | Get the dj-webmachine code:: 34 | 35 | $ git clone https://github.com/benoitc/dj-webmachine.git 36 | $ cd dj-webmachine 37 | 38 | Or using a tarbal:: 39 | 40 | $ wget http://github.com/benoitc/dj-webmachine/tarball/master -o dj-webmachine.tar.gz 41 | $ tar xvzf dj-webmachine.tar.gz 42 | $ cd dj-webmachine-$HASH/ 43 | 44 | and install:: 45 | 46 | $ sudo python setup.py install 47 | 48 | 49 | dj-webmachine in 5 minutes 50 | ++++++++++++++++++++++++++ 51 | 52 | We will quickly create an Hello world accepting HTML and JSON. 53 | 54 | :: 55 | 56 | $ django-admin startproject helloworld 57 | $ cd helloworld 58 | $ python manage.py startapp hello 59 | 60 | In the hello folder create a file named ``resources.py``:: 61 | 62 | import json 63 | from webmachine import Resource 64 | 65 | class Hello(Resource): 66 | 67 | def content_types_provided(self, req, resp): 68 | """" define the content type we render accoridng the Accept 69 | header. 70 | """ 71 | return ( 72 | ("", self.to_html), 73 | ("application/json", self.to_json) 74 | ) 75 | 76 | def to_html(self, req, resp): 77 | return "Hello world!\n" 78 | 79 | def to_json(self, req, resp): 80 | return "%s\n" % json.dumps({"message": "hello world!", "ok": True}) 81 | 82 | Add **dj-webmachine** and your hello app to ``INSTALLED_APPS`` in your 83 | settings:: 84 | 85 | INSTALLED_APPS = ( 86 | ... 87 | 'webmachine', 88 | 'helloworld.hello' 89 | ) 90 | 91 | Put your the Hello resource in your ``urls.py``:: 92 | 93 | from django.conf.urls.defaults import * 94 | 95 | from helloworld.hello.resource import Hello 96 | 97 | urlpatterns = patterns('', 98 | (r'^$', Hello()), 99 | ) 100 | 101 | Launch your application:: 102 | 103 | $ python manage.py runserver 104 | 105 | Take a look! Point a web browser at http://localhost:8000/ 106 | 107 | Or with curl:: 108 | 109 | $ curl http://127.0.0.1:8000 110 | Hello world! 111 | 112 | $ curl http://127.0.0.1:8000 -H "Accept: application/json" 113 | {"message": "hello world!", "ok": true} 114 | 115 | 116 | 117 | The first line ask the hello page as html while the second using the 118 | same url ask for JSON. 119 | 120 | To learn how to do more interresting things, checkout `some examples `_ or read `more documentations `_ . 121 | 122 | .. _Python: http://python.org 123 | .. _Django: http://djangoproject.org 124 | -------------------------------------------------------------------------------- /doc/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 | BUILDDIR = build 9 | SPHINXTOGITHUB = ${PWD}/sphinxtogithub.py 10 | GHPIMPORT = ${PWD}/ghp-import 11 | 12 | # Internal variables. 13 | PAPEROPT_a4 = -D latex_paper_size=a4 14 | PAPEROPT_letter = -D latex_paper_size=letter 15 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | 18 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 19 | 20 | help: 21 | @echo "Please use \`make ' where is one of" 22 | @echo " html to make standalone HTML files" 23 | @echo " dirhtml to make HTML files named index.html in directories" 24 | @echo " singlehtml to make a single large HTML file" 25 | @echo " pickle to make pickle files" 26 | @echo " json to make JSON files" 27 | @echo " htmlhelp to make HTML files and a HTML help project" 28 | @echo " qthelp to make HTML files and a qthelp project" 29 | @echo " devhelp to make HTML files and a Devhelp project" 30 | @echo " epub to make an epub" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " text to make text files" 34 | @echo " man to make manual pages" 35 | @echo " changes to make an overview of all changed/added/deprecated items" 36 | @echo " linkcheck to check all external links for integrity" 37 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 38 | 39 | clean: 40 | -rm -rf $(BUILDDIR)/* 41 | 42 | html: clean 43 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 44 | @echo 45 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 46 | 47 | github: 48 | @echo "Send to github" 49 | $(GHPIMPORT) -p $(BUILDDIR)/html 50 | 51 | dirhtml: 52 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 53 | @echo 54 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 55 | 56 | singlehtml: 57 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 58 | @echo 59 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 60 | 61 | pickle: 62 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 63 | @echo 64 | @echo "Build finished; now you can process the pickle files." 65 | 66 | json: 67 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 68 | @echo 69 | @echo "Build finished; now you can process the JSON files." 70 | 71 | htmlhelp: 72 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 73 | @echo 74 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 75 | ".hhp project file in $(BUILDDIR)/htmlhelp." 76 | 77 | qthelp: 78 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 79 | @echo 80 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 81 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 82 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/dj-webmachine.qhcp" 83 | @echo "To view the help file:" 84 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/dj-webmachine.qhc" 85 | 86 | devhelp: 87 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 88 | @echo 89 | @echo "Build finished." 90 | @echo "To view the help file:" 91 | @echo "# mkdir -p $$HOME/.local/share/devhelp/dj-webmachine" 92 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/dj-webmachine" 93 | @echo "# devhelp" 94 | 95 | epub: 96 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 97 | @echo 98 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 99 | 100 | latex: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo 103 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 104 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 105 | "(use \`make latexpdf' here to do that automatically)." 106 | 107 | latexpdf: 108 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 109 | @echo "Running LaTeX files through pdflatex..." 110 | make -C $(BUILDDIR)/latex all-pdf 111 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 112 | 113 | text: 114 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 115 | @echo 116 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 117 | 118 | man: 119 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 120 | @echo 121 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 122 | 123 | changes: 124 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 125 | @echo 126 | @echo "The overview file is in $(BUILDDIR)/changes." 127 | 128 | linkcheck: 129 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 130 | @echo 131 | @echo "Link check complete; look for any errors in the above output " \ 132 | "or in $(BUILDDIR)/linkcheck/output.txt." 133 | 134 | doctest: 135 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 136 | @echo "Testing of doctests in the sources finished, look at the " \ 137 | "results in $(BUILDDIR)/doctest/output.txt." 138 | -------------------------------------------------------------------------------- /doc/ghp-import: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | # This file is part of the ghp-import package released under 4 | # the Tumbolia Public License. See the LICENSE file for more 5 | # information. 6 | 7 | import optparse as op 8 | import os 9 | import subprocess as sp 10 | import time 11 | 12 | __usage__ = "%prog [OPTIONS] DIRECTORY" 13 | 14 | def is_repo(d): 15 | if not os.path.isdir(d): 16 | return False 17 | if not os.path.isdir(os.path.join(d, 'objects')): 18 | return False 19 | if not os.path.isdir(os.path.join(d, 'refs')): 20 | return False 21 | 22 | headref = os.path.join(d, 'HEAD') 23 | if os.path.isfile(headref): 24 | return True 25 | if os.path.islinke(headref) and os.readlink(headref).startswith("refs"): 26 | return True 27 | return False 28 | 29 | def find_repo(path): 30 | if is_repo(path): 31 | return True 32 | if is_repo(os.path.join(path, '.git')): 33 | return True 34 | (parent, ignore) = os.path.split(path) 35 | if parent == path: 36 | return False 37 | return find_repo(parent) 38 | 39 | def try_rebase(remote): 40 | cmd = ['git', 'rev-list', '--max-count=1', 'origin/gh-pages'] 41 | p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) 42 | (rev, ignore) = p.communicate() 43 | if p.wait() != 0: 44 | return True 45 | cmd = ['git', 'update-ref', 'refs/heads/gh-pages', rev.strip()] 46 | if sp.call(cmd) != 0: 47 | return False 48 | return True 49 | 50 | def get_config(key): 51 | p = sp.Popen(['git', 'config', key], stdin=sp.PIPE, stdout=sp.PIPE) 52 | (value, stderr) = p.communicate() 53 | return value.strip() 54 | 55 | def get_prev_commit(): 56 | cmd = ['git', 'rev-list', '--max-count=1', 'gh-pages'] 57 | p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) 58 | (rev, ignore) = p.communicate() 59 | if p.wait() != 0: 60 | return None 61 | return rev.strip() 62 | 63 | def make_when(timestamp=None): 64 | if timestamp is None: 65 | timestamp = int(time.time()) 66 | currtz = "%+05d" % (time.timezone / 36) # / 3600 * 100 67 | return "%s %s" % (timestamp, currtz) 68 | 69 | def start_commit(pipe, message): 70 | username = get_config("user.name") 71 | email = get_config("user.email") 72 | pipe.stdin.write('commit refs/heads/gh-pages\n') 73 | pipe.stdin.write('committer %s <%s> %s\n' % (username, email, make_when())) 74 | pipe.stdin.write('data %d\n%s\n' % (len(message), message)) 75 | head = get_prev_commit() 76 | if head: 77 | pipe.stdin.write('from %s\n' % head) 78 | pipe.stdin.write('deleteall\n') 79 | 80 | def add_file(pipe, srcpath, tgtpath): 81 | pipe.stdin.write('M 100644 inline %s\n' % tgtpath) 82 | with open(srcpath) as handle: 83 | data = handle.read() 84 | pipe.stdin.write('data %d\n' % len(data)) 85 | pipe.stdin.write(data) 86 | pipe.stdin.write('\n') 87 | 88 | def run_import(srcdir, message): 89 | cmd = ['git', 'fast-import', '--date-format=raw', '--quiet'] 90 | pipe = sp.Popen(cmd, stdin=sp.PIPE) 91 | start_commit(pipe, message) 92 | for path, dnames, fnames in os.walk(srcdir): 93 | for fn in fnames: 94 | fpath = os.path.join(path, fn) 95 | add_file(pipe, fpath, os.path.relpath(fpath, start=srcdir)) 96 | pipe.stdin.write('\n') 97 | pipe.stdin.close() 98 | if pipe.wait() != 0: 99 | print "Failed to process commit." 100 | 101 | def options(): 102 | return [ 103 | op.make_option('-m', dest='mesg', default='Update documentation', 104 | help='The commit message to use on the gh-pages branch.'), 105 | op.make_option('-p', dest='push', default=False, action='store_true', 106 | help='Push the branch to origin/gh-pages after committing.'), 107 | op.make_option('-r', dest='remote', default='origin', 108 | help='The name of the remote to push to. [%default]') 109 | ] 110 | 111 | def main(): 112 | parser = op.OptionParser(usage=__usage__, option_list=options()) 113 | opts, args = parser.parse_args() 114 | 115 | if len(args) == 0: 116 | parser.error("No import directory specified.") 117 | 118 | if len(args) > 1: 119 | parser.error("Unknown arguments specified: %s" % ', '.join(args[1:])) 120 | 121 | if not os.path.isdir(args[0]): 122 | parser.error("Not a directory: %s" % args[0]) 123 | 124 | if not find_repo(os.getcwd()): 125 | parser.error("No Git repository found.") 126 | 127 | if not try_rebase(opts.remote): 128 | parser.error("Failed to rebase gh-pages branch.") 129 | 130 | run_import(args[0], opts.mesg) 131 | 132 | if opts.push: 133 | sp.check_call(['git', 'push', opts.remote, 'gh-pages']) 134 | 135 | if __name__ == '__main__': 136 | main() 137 | 138 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | if NOT "%PAPER%" == "" ( 11 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 12 | ) 13 | 14 | if "%1" == "" goto help 15 | 16 | if "%1" == "help" ( 17 | :help 18 | echo.Please use `make ^` where ^ is one of 19 | echo. html to make standalone HTML files 20 | echo. dirhtml to make HTML files named index.html in directories 21 | echo. singlehtml to make a single large HTML file 22 | echo. pickle to make pickle files 23 | echo. json to make JSON files 24 | echo. htmlhelp to make HTML files and a HTML help project 25 | echo. qthelp to make HTML files and a qthelp project 26 | echo. devhelp to make HTML files and a Devhelp project 27 | echo. epub to make an epub 28 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 29 | echo. text to make text files 30 | echo. man to make manual pages 31 | echo. changes to make an overview over all changed/added/deprecated items 32 | echo. linkcheck to check all external links for integrity 33 | echo. doctest to run all doctests embedded in the documentation if enabled 34 | goto end 35 | ) 36 | 37 | if "%1" == "clean" ( 38 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 39 | del /q /s %BUILDDIR%\* 40 | goto end 41 | ) 42 | 43 | if "%1" == "html" ( 44 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 45 | echo. 46 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 47 | goto end 48 | ) 49 | 50 | if "%1" == "dirhtml" ( 51 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 52 | echo. 53 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 54 | goto end 55 | ) 56 | 57 | if "%1" == "singlehtml" ( 58 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 59 | echo. 60 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 61 | goto end 62 | ) 63 | 64 | if "%1" == "pickle" ( 65 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 66 | echo. 67 | echo.Build finished; now you can process the pickle files. 68 | goto end 69 | ) 70 | 71 | if "%1" == "json" ( 72 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 73 | echo. 74 | echo.Build finished; now you can process the JSON files. 75 | goto end 76 | ) 77 | 78 | if "%1" == "htmlhelp" ( 79 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 80 | echo. 81 | echo.Build finished; now you can run HTML Help Workshop with the ^ 82 | .hhp project file in %BUILDDIR%/htmlhelp. 83 | goto end 84 | ) 85 | 86 | if "%1" == "qthelp" ( 87 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 88 | echo. 89 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 90 | .qhcp project file in %BUILDDIR%/qthelp, like this: 91 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\dj-webmachine.qhcp 92 | echo.To view the help file: 93 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\dj-webmachine.ghc 94 | goto end 95 | ) 96 | 97 | if "%1" == "devhelp" ( 98 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 99 | echo. 100 | echo.Build finished. 101 | goto end 102 | ) 103 | 104 | if "%1" == "epub" ( 105 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 106 | echo. 107 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 108 | goto end 109 | ) 110 | 111 | if "%1" == "latex" ( 112 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 113 | echo. 114 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 115 | goto end 116 | ) 117 | 118 | if "%1" == "text" ( 119 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 120 | echo. 121 | echo.Build finished. The text files are in %BUILDDIR%/text. 122 | goto end 123 | ) 124 | 125 | if "%1" == "man" ( 126 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 127 | echo. 128 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 129 | goto end 130 | ) 131 | 132 | if "%1" == "changes" ( 133 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 134 | echo. 135 | echo.The overview file is in %BUILDDIR%/changes. 136 | goto end 137 | ) 138 | 139 | if "%1" == "linkcheck" ( 140 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 141 | echo. 142 | echo.Link check complete; look for any errors in the above output ^ 143 | or in %BUILDDIR%/linkcheck/output.txt. 144 | goto end 145 | ) 146 | 147 | if "%1" == "doctest" ( 148 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 149 | echo. 150 | echo.Testing of doctests in the sources finished, look at the ^ 151 | results in %BUILDDIR%/doctest/output.txt. 152 | goto end 153 | ) 154 | 155 | :end 156 | -------------------------------------------------------------------------------- /doc/source/_static/http-headers-status-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/dj-webmachine/77653d73de57388b712eaf50de8c32ec70c182fa/doc/source/_static/http-headers-status-v3.png -------------------------------------------------------------------------------- /doc/source/auth.rst: -------------------------------------------------------------------------------- 1 | .. _auth: 2 | 3 | Handle authorization 4 | -------------------- 5 | 6 | webmachine offer mechanism to authenticate via basic authentication or 7 | oauth. All authentication module should inherit from 8 | :class:`webmachine.auth.base.Auth`. 9 | 10 | .. code-block:: python 11 | 12 | class Auth(object): 13 | 14 | def authorized(self, request): 15 | return True 16 | 17 | Basic Authentification 18 | ++++++++++++++++++++++ 19 | 20 | To handle basic authentication just do this in your ``Resource`` 21 | class: 22 | 23 | .. code-block:: python 24 | 25 | from webmachine import Resource 26 | from webmachine.auth import BasicAuth 27 | 28 | class MyResource(Resource): 29 | 30 | ... 31 | 32 | def is_authorized(self, req, resp): 33 | return BasicAuth().authorized(req, resp) 34 | 35 | OAUTH 36 | +++++ 37 | 38 | .. warning:: 39 | 40 | You need to install restkit_ >= 3.0.2 to have oauth in your application. 41 | 42 | .. _restkit: http://benoitc.github.com/restkit 43 | 44 | It's easy to handle oauth in your dj-webmachine application. First you 45 | need to add the oauth resource to your ``urls.py`` file: 46 | 47 | .. code-block:: python 48 | 49 | from webmachine.auth import oauth_res 50 | 51 | urlpatterns = patterns('', 52 | 53 | ... 54 | 55 | (r'^auth/', include(oauth_res.OauthResource().get_urls())), 56 | ) 57 | 58 | Then like with the basic authentication add the class 59 | :class:`webmachine.auth.oauth.Oauth` in your resource: 60 | 61 | .. code-block:: python 62 | 63 | from webmachine import Resource 64 | from webmachine.authoauth import Oauth 65 | 66 | class MyResource(Resource): 67 | 68 | ... 69 | 70 | def is_authorized(self, req, resp): 71 | return Oauth().authorized(req, resp) 72 | 73 | Test it 74 | ~~~~~~~ 75 | 76 | Create a consumer 77 | 78 | .. code-block:: python 79 | 80 | >>> from webmachine.models import Consumer 81 | >>> consumer = Consumer.objects.create_consumer() 82 | 83 | 84 | Request a token for this consumer. We use the restkit_ client to do that. 85 | 86 | 87 | .. code-block:: python 88 | 89 | >>> from restkit import request 90 | >>> from restkit.filters import OAuthFilter 91 | >>> from restkit.oauth2 import Token 92 | >>> resp = request("http://127.0.0.1:8000/auth/request_token", 93 | filters=[OAuthFilter("*", consumer)]) 94 | >>> resp.status_int 95 | 200 96 | 97 | You need now to read the response body, and create a token request on 98 | the client side: 99 | 100 | .. code-block:: python 101 | 102 | >>> import urlparse 103 | >>> qs = urlparse.parse_qs(resp.body_string()) 104 | >>> request_token = Token(qs['oauth_token'][0], qs['oauth_token_secret'][0]) 105 | 106 | Now we need to authorize our client: 107 | 108 | .. code-block:: python 109 | 110 | >>> resp = request("http://127.0.0.1:8000/auth/authorize", 111 | filters=[OAuthFilter("*", consumer, request_token)]) 112 | 113 | We now need to read the body for the second step, since we send a form 114 | here to the client. We are again using restkit to send the result of the 115 | form and get the tokens back. 116 | 117 | .. code-block:: python 118 | 119 | >>> form = {'authorize_access': 1, 'oauth_token': '2e8bfdaddb664d97ada4b8cec6827bcf', 120 | 'csrf_signature': 'iGHjuOEppMeg+gHWyYV/etLRxTQ='} 121 | >>> resp = request("http://127.0.0.1:8000/auth/authorize", method="POST", 122 | body=form, filters=[OAuthFilter("*", consumer, request_token)]) 123 | 124 | We are now authorized, and we need to ask an access token: 125 | 126 | .. code-block:: python 127 | 128 | >>> resp = request("http://127.0.0.1:8000/auth/access_token", 129 | filters=[OAuthFilter("*", consumer, request_token)]) 130 | >>> qs = urlparse.parse_qs(resp.body_string()) 131 | >>> access_token = Token(qs['oauth_token'][0], qs['oauth_token_secret'][0]) 132 | 133 | If you use tthe testoauth application in ``examples`` folder, you can 134 | then test if you are authorized. Without the acces token: 135 | 136 | .. code-block:: python 137 | 138 | >>> resp = request("http://127.0.0.1:8000/") 139 | >>> resp.status_int 140 | 401 141 | >>> resp.headers 142 | {'date': 'Thu, 11 Nov 2010 23:16:25 GMT', 'content-type': 'text/html', 143 | 'www-authenticate': 'OAuth realm="OAuth"', 'server': 'WSGIServer/0.1 Python/2.7'} 144 | 145 | You can see the **WWW-Authenticate** header. 146 | 147 | Using the access token: 148 | 149 | .. code-block:: python 150 | 151 | >>> resp = request("http://127.0.0.1:8000/", 152 | filters=[OAuthFilter("*", consumer, access_token)]) 153 | >>> resp.status_int 154 | 200 155 | >>> resp.body_string() 156 | "

I'm protected you know.

" 157 | 158 | 159 | AUTH classes description 160 | ++++++++++++++++++++++++ 161 | 162 | Basic authentication 163 | ~~~~~~~~~~~~~~~~~~~~ 164 | 165 | .. autoclass:: webmachine.auth.BasicAuth 166 | :members: 167 | :undoc-members: 168 | 169 | Oauth authentication 170 | ~~~~~~~~~~~~~~~~~~~~ 171 | 172 | .. autoclass:: webmachine.auth.oauth.Oauth 173 | :members: 174 | :undoc-members: 175 | 176 | Datastore 177 | ~~~~~~~~~ 178 | 179 | Find here the description of the abstract class you need to use if you 180 | want to create your own class. This class mmanage creation and request 181 | of tokens, nonces and consumer. See the 182 | :class:`webmachine.auth.oauth_datastore.DataStore` for a complete 183 | example. To use you own class add it to your settings: 184 | 185 | .. code-block:: python 186 | 187 | OAUTH_DATASTORE = 'webmachine.auth.oauth_store.DataStore' 188 | 189 | .. autoclass:: webmachine.auth.oauth_store.OAuthDataStore 190 | :members: 191 | :undoc-members: 192 | 193 | .. autoclass:: webmachine.auth.oauth_store.DataStore 194 | :members: 195 | :undoc-members: 196 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import sys, os 7 | import webmachine 8 | 9 | 10 | sys.path.insert(0, os.path.abspath('.')) 11 | os.environ['DJANGO_SETTINGS_MODULE'] = 'django.conf.global_settings' 12 | 13 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 14 | 'sphinx.ext.viewcode', 'sphinxtogithub'] 15 | 16 | 17 | templates_path = ['_templates'] 18 | 19 | source_suffix = '.rst' 20 | 21 | master_doc = 'index' 22 | 23 | project = u'dj-webmachine' 24 | copyright = u'2010, Benoît Chesnea u' 25 | 26 | version = webmachine.__version__ 27 | release = version 28 | 29 | exclude_trees = ['_build'] 30 | 31 | pygments_style = 'sphinx' 32 | html_theme = 'default' 33 | html_static_path = ['_static'] 34 | htmlhelp_basename = 'dj-webmachinedoc' 35 | 36 | latex_documents = [ 37 | ('index', 'dj-webmachine.tex', u'dj-webmachine Documentation', 38 | u'Benoît Chesneau', 'manual'), 39 | ] 40 | 41 | man_pages = [ 42 | ('index', 'dj-webmachine', u'dj-webmachine Documentation', 43 | [u'Benoît Chesneau'], 1) 44 | ] 45 | 46 | epub_title = u'dj-webmachine' 47 | epub_author = u'Benoît Chesneau' 48 | epub_publisher = u'Benoît Chesneau' 49 | epub_copyright = u'2010, Benoît Chesneau' 50 | 51 | 52 | -------------------------------------------------------------------------------- /doc/source/diagram.rst: -------------------------------------------------------------------------------- 1 | .. _diagram: 2 | 3 | dj-webmachine decision flow 4 | --------------------------- 5 | 6 | This diagram is illustrative of the flow of processing that a :ref:`webmachine resource ` 7 | goes through from inception to response. 8 | 9 | Original com from `webmachine site `_. 10 | 11 | .. image:: _static/http-headers-status-v3.png 12 | -------------------------------------------------------------------------------- /doc/source/docs.rst: -------------------------------------------------------------------------------- 1 | .. _docs: 2 | 3 | Documentation 4 | ------------- 5 | 6 | .. toctree:: 7 | :maxdepth: 1 8 | 9 | quickstart 10 | resources 11 | wm 12 | auth 13 | throttling 14 | recipes 15 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. dj-webmachine documentation master file, created by 2 | sphinx-quickstart on Wed Nov 10 16:19:42 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to dj-webmachine's documentation! 7 | ========================================= 8 | 9 | webmachine provides a REST toolkit based on Django. It's heavily 10 | inspired on `webmachine `_ from Basho. 11 | 12 | 13 | dj-webmachine is an application layer that adds HTTP semantic awareness on 14 | top of Django and provides a simple and clean way to connect that to 15 | your applications' behavior. dj-webmachine also offers you the 16 | possibility to build simple API based on your model and the tools to 17 | create automatically docs and clients from it (work in progress). 18 | 19 | Contents: 20 | 21 | .. toctree:: 22 | :maxdepth: 1 23 | 24 | introduction 25 | docs 26 | 27 | 28 | Resource oriented 29 | ----------------- 30 | 31 | A dj-webmachine application is a set of :ref:`Resources objects`, each of which 32 | is a set of methods over the state of the resource. 33 | 34 | .. code-block:: python 35 | 36 | from webmachine import Resource 37 | 38 | class Hello(Resource): 39 | 40 | def to_html(self, req, resp): 41 | return "Hello world!\n" 42 | 43 | These methodes give you a place to define the representations and other 44 | Web-relevant properties of your application's resources. 45 | 46 | For most of dj-webmachine applications, most of the Resources instance 47 | are small and isolated. The web behavior introduced :ref:`by directly mapping the HTTP ` 48 | make your application easy to debug and read. 49 | 50 | Simple Routing 51 | -------------- 52 | 53 | Combinating the power of Django and the resources it’s relatively easy to buid an api. The process is also eased using the WM object. dj-webmachine offer a way to create automatically resources by using :ref:`the route decorator`. 54 | 55 | .. code-block:: python 56 | 57 | from webmachine.ap import wm 58 | 59 | import json 60 | @wm.route(r"^$") 61 | def hello(req, resp): 62 | return "

hello world!

" 63 | 64 | 65 | 66 | Indices and tables 67 | ================== 68 | 69 | * :ref:`genindex` 70 | * :ref:`modindex` 71 | * :ref:`search` 72 | 73 | -------------------------------------------------------------------------------- /doc/source/introduction.rst: -------------------------------------------------------------------------------- 1 | .. _intro: 2 | 3 | How do I get Started? 4 | --------------------- 5 | 6 | If you want to jump in and start coding right away, the :ref:`quickstart ` is the way to go. 7 | 8 | You could also watch this screencast: 9 | 10 | .. raw:: html 11 | 12 |

dj-webmachine sneak preview from Benoit Chesneau on Vimeo.

13 | -------------------------------------------------------------------------------- /doc/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | getting started quickly with dj-webmachine 4 | ------------------------------------------ 5 | 6 | 7 | Install 8 | +++++++ 9 | 10 | Make sure that you have a working Python_ 2.x >=2.5 installed and Django_ >= 1.1. 11 | 12 | 13 | With pip 14 | ~~~~~~~~ 15 | 16 | :: 17 | 18 | $ pip install dj-webmachine 19 | 20 | From source 21 | ~~~~~~~~~~~ 22 | 23 | Get the dj-webmachine code:: 24 | 25 | $ git clone https://github.com/benoitc/dj-webmachine.git 26 | $ cd dj-webmachine 27 | 28 | Or using a tarbal:: 29 | 30 | $ wget http://github.com/benoitc/dj-webmachine/tarball/master -o dj-webmachine.tar.gz 31 | $ tar xvzf dj-webmachine.tar.gz 32 | $ cd dj-webmachine-$HASH/ 33 | 34 | and install:: 35 | 36 | $ sudo python setup.py install 37 | 38 | 39 | Create a django project 40 | +++++++++++++++++++++++ 41 | 42 | We will quickly create an Hello world accepting HTML and JSON. 43 | 44 | $ django-admin startproject helloworld 45 | $ cd helloworld 46 | $ python manage.py startapp hello 47 | 48 | In the hello folder create a file named ``resource.p```: 49 | 50 | .. code-block:: python 51 | 52 | import json 53 | from webmachine import Resource 54 | 55 | class Hello(Resource): 56 | 57 | def content_types_provided(self, req, resp): 58 | """" define the content type we render accoridng the Accept 59 | header. 60 | """ 61 | return ( 62 | ("", self.to_html), 63 | ("application/json", self.to_json) 64 | ) 65 | 66 | def to_html(self, req, resp): 67 | return "Hello world!\n" 68 | 69 | def to_json(self, req, resp): 70 | return "%s\n" % json.dumps({"message": "hello world!", "ok": True}) 71 | 72 | Add **dj-webmachine** and your hello app to ``INSTALLED_APPS`` in your 73 | settings:: 74 | 75 | INSTALLED_APPS = ( 76 | ... 77 | 'webmachine', 78 | 'helloworld.hello' 79 | ) 80 | 81 | Put your the Hello resource in your ``urls.py``: 82 | 83 | .. code-block:: python 84 | 85 | from django.conf.urls.defaults import * 86 | 87 | from helloworld.hello.resources import Hello 88 | 89 | urlpatterns = patterns('', 90 | (r'^$', Hello()), 91 | ) 92 | 93 | Launch your application:: 94 | 95 | $ python manage.py runserver 96 | 97 | Take a look! Point a web browser at http://localhost:8000/ 98 | 99 | Or with curl:: 100 | 101 | $ curl http://127.0.0.1:8000 102 | Hello world! 103 | 104 | $ curl http://127.0.0.1:8000 -H "Accept: application/json" 105 | {"message": "hello world!", "ok": true} 106 | 107 | 108 | 109 | The first line ask the hello page as html while the second using the 110 | same url ask for JSON. 111 | 112 | To learn how to do more interresting things, checkout :ref:`some examples ` or read :ref:`more documentations ` . 113 | 114 | .. _Python: http://python.org 115 | .. _Django: http://djangoproject.org 116 | -------------------------------------------------------------------------------- /doc/source/recipes.rst: -------------------------------------------------------------------------------- 1 | .. _recipe: 2 | 3 | Recipes 4 | ------- 5 | 6 | Some quick recipes to show the dj-webmachine usage. 7 | 8 | Get path named arguments 9 | ++++++++++++++++++++++++ 10 | 11 | if you have some named arguments in your url pattern, you can get them 12 | using the ``url_kwargs`` member of the req object:: 13 | 14 | kwargs = req.url_kwargs 15 | 16 | Simplest working resource 17 | +++++++++++++++++++++++++ 18 | 19 | This simple resource only return HTML 20 | 21 | .. code-block:: python 22 | 23 | class MyResource(Resource): 24 | 25 | def to_html(self, req, resp): 26 | return "

Hello World!

" 27 | 28 | Return different content types on GET 29 | +++++++++++++++++++++++++++++++++++++ 30 | 31 | Suppose you want to serve plaintexts and html clients on valid GET 32 | requests: 33 | 34 | .. code-block:: python 35 | 36 | class MyResource(Resource): 37 | 38 | def content_types_provided(self, req, resp): 39 | return [ 40 | ("text/html", self.to_html), 41 | ("text/plain", self.to_text) 42 | ] 43 | 44 | def to_html(self, req, resp): 45 | return "

Hello World!

" 46 | 47 | def to_text(self, req, resp): 48 | return "Hello World!" 49 | 50 | Handle POST using the resource 51 | ++++++++++++++++++++++++++++++ 52 | 53 | .. code-block:: python 54 | 55 | class MyResource(Resource): 56 | 57 | def allowed_methods(self, req, resp): 58 | return ['POST'] 59 | 60 | def content_types_accepted(self, req, resp): 61 | return [('application/json', self.to_json)] 62 | 63 | def to_json(self, req, resp): 64 | body = json.loads(req.raw_post_data) 65 | resp.content = json.dumps(json.dumps(body)) 66 | 67 | def post_is_create(self, req, resp): 68 | return True 69 | 70 | Handle GET using the decorator 71 | ++++++++++++++++++++++++++++++ 72 | 73 | .. code-block:: python 74 | 75 | @wm.route(r"taroute$", 76 | methods=['GET', 'HEAD'], 77 | provided=[('text/html', 'text/plain')]) 78 | def fetched(req, resp): 79 | if resp.content_type == "text/html": 80 | return "

Hello World!

" 81 | return "Hello World!" 82 | 83 | Handle POST using the decorator 84 | +++++++++++++++++++++++++++++++ 85 | 86 | Same code for other methods. You can check the method using 87 | ``req.method`` 88 | 89 | .. code-block:: python 90 | 91 | # with teh decorator 92 | @wm.route(r"taroute$", 93 | methods="POST", 94 | accepted=[('application/json', json.loads)], 95 | provided=[('application/json', json.dumps)]) 96 | def posted(req, resp): 97 | # my body has been deserialized 98 | body = req.raw_post_data 99 | 100 | # my body will be serialized 101 | return body 102 | -------------------------------------------------------------------------------- /doc/source/resources.rst: -------------------------------------------------------------------------------- 1 | .. _resources: 2 | 3 | dj-webmachine Resource object methods 4 | ------------------------------------- 5 | 6 | All dj-webmachine resources should inherit from the 7 | :class:`webmachine.Resource` class: 8 | 9 | .. code-block:: python 10 | 11 | from webmachine import Resource 12 | 13 | class MyResource(Resource): 14 | """ my app resource """ 15 | 16 | 17 | **Resource methods** are of the signature: 18 | 19 | .. code-block:: python 20 | 21 | def f(self, req, resp): 22 | return result 23 | 24 | ``req`` is a :class:`django.http.HttpRequest` instance, and ``resp`` a 25 | :class:`django.http.HttpResource` instance. This instances have been 26 | :ref:`improved to support more HTTP semantics `. At any time you 27 | can manipulate this object to return the response you want or pass 28 | values to other methods. 29 | 30 | There are over 30 Resource methods you can define, but any of them can 31 | be omitted as they have reasonable defaults. 32 | 33 | .. _resdesr: 34 | 35 | Resource methods description 36 | ++++++++++++++++++++++++++++ 37 | 38 | .. autoclass:: webmachine.Resource 39 | :members: 40 | :undoc-members: 41 | 42 | -------------------------------------------------------------------------------- /doc/source/sphinxtogithub.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import optparse as op 4 | import os 5 | import sys 6 | import shutil 7 | 8 | 9 | class NoDirectoriesError(Exception): 10 | "Error thrown when no directories starting with an underscore are found" 11 | 12 | class DirHelper(object): 13 | 14 | def __init__(self, is_dir, list_dir, walk, rmtree): 15 | 16 | self.is_dir = is_dir 17 | self.list_dir = list_dir 18 | self.walk = walk 19 | self.rmtree = rmtree 20 | 21 | class FileSystemHelper(object): 22 | 23 | def __init__(self, open_, path_join, move, exists): 24 | 25 | self.open_ = open_ 26 | self.path_join = path_join 27 | self.move = move 28 | self.exists = exists 29 | 30 | class Replacer(object): 31 | "Encapsulates a simple text replace" 32 | 33 | def __init__(self, from_, to): 34 | 35 | self.from_ = from_ 36 | self.to = to 37 | 38 | def process(self, text): 39 | 40 | return text.replace( self.from_, self.to ) 41 | 42 | class FileHandler(object): 43 | "Applies a series of replacements the contents of a file inplace" 44 | 45 | def __init__(self, name, replacers, opener): 46 | 47 | self.name = name 48 | self.replacers = replacers 49 | self.opener = opener 50 | 51 | def process(self): 52 | 53 | text = self.opener(self.name).read() 54 | 55 | for replacer in self.replacers: 56 | text = replacer.process( text ) 57 | 58 | self.opener(self.name, "w").write(text) 59 | 60 | class Remover(object): 61 | 62 | def __init__(self, exists, remove): 63 | self.exists = exists 64 | self.remove = remove 65 | 66 | def __call__(self, name): 67 | 68 | if self.exists(name): 69 | self.remove(name) 70 | 71 | class ForceRename(object): 72 | 73 | def __init__(self, renamer, remove): 74 | 75 | self.renamer = renamer 76 | self.remove = remove 77 | 78 | def __call__(self, from_, to): 79 | 80 | self.remove(to) 81 | self.renamer(from_, to) 82 | 83 | class VerboseRename(object): 84 | 85 | def __init__(self, renamer, stream): 86 | 87 | self.renamer = renamer 88 | self.stream = stream 89 | 90 | def __call__(self, from_, to): 91 | 92 | self.stream.write( 93 | "Renaming directory '%s' -> '%s'\n" 94 | % (os.path.basename(from_), os.path.basename(to)) 95 | ) 96 | 97 | self.renamer(from_, to) 98 | 99 | 100 | class DirectoryHandler(object): 101 | "Encapsulates renaming a directory by removing its first character" 102 | 103 | def __init__(self, name, root, renamer): 104 | 105 | self.name = name 106 | self.new_name = name[1:] 107 | self.root = root + os.sep 108 | self.renamer = renamer 109 | 110 | def path(self): 111 | 112 | return os.path.join(self.root, self.name) 113 | 114 | def relative_path(self, directory, filename): 115 | 116 | path = directory.replace(self.root, "", 1) 117 | return os.path.join(path, filename) 118 | 119 | def new_relative_path(self, directory, filename): 120 | 121 | path = self.relative_path(directory, filename) 122 | return path.replace(self.name, self.new_name, 1) 123 | 124 | def process(self): 125 | 126 | from_ = os.path.join(self.root, self.name) 127 | to = os.path.join(self.root, self.new_name) 128 | self.renamer(from_, to) 129 | 130 | 131 | class HandlerFactory(object): 132 | 133 | def create_file_handler(self, name, replacers, opener): 134 | 135 | return FileHandler(name, replacers, opener) 136 | 137 | def create_dir_handler(self, name, root, renamer): 138 | 139 | return DirectoryHandler(name, root, renamer) 140 | 141 | 142 | class OperationsFactory(object): 143 | 144 | def create_force_rename(self, renamer, remover): 145 | 146 | return ForceRename(renamer, remover) 147 | 148 | def create_verbose_rename(self, renamer, stream): 149 | 150 | return VerboseRename(renamer, stream) 151 | 152 | def create_replacer(self, from_, to): 153 | 154 | return Replacer(from_, to) 155 | 156 | def create_remover(self, exists, remove): 157 | 158 | return Remover(exists, remove) 159 | 160 | 161 | class Layout(object): 162 | """ 163 | Applies a set of operations which result in the layout 164 | of a directory changing 165 | """ 166 | 167 | def __init__(self, directory_handlers, file_handlers): 168 | 169 | self.directory_handlers = directory_handlers 170 | self.file_handlers = file_handlers 171 | 172 | def process(self): 173 | 174 | for handler in self.file_handlers: 175 | handler.process() 176 | 177 | for handler in self.directory_handlers: 178 | handler.process() 179 | 180 | 181 | class LayoutFactory(object): 182 | "Creates a layout object" 183 | 184 | def __init__(self, operations_factory, handler_factory, file_helper, dir_helper, verbose, stream, force): 185 | 186 | self.operations_factory = operations_factory 187 | self.handler_factory = handler_factory 188 | 189 | self.file_helper = file_helper 190 | self.dir_helper = dir_helper 191 | 192 | self.verbose = verbose 193 | self.output_stream = stream 194 | self.force = force 195 | 196 | def create_layout(self, path): 197 | 198 | contents = self.dir_helper.list_dir(path) 199 | 200 | renamer = self.file_helper.move 201 | 202 | if self.force: 203 | remove = self.operations_factory.create_remover(self.file_helper.exists, self.dir_helper.rmtree) 204 | renamer = self.operations_factory.create_force_rename(renamer, remove) 205 | 206 | if self.verbose: 207 | renamer = self.operations_factory.create_verbose_rename(renamer, self.output_stream) 208 | 209 | # Build list of directories to process 210 | directories = [d for d in contents if self.is_underscore_dir(path, d)] 211 | underscore_directories = [ 212 | self.handler_factory.create_dir_handler(d, path, renamer) 213 | for d in directories 214 | ] 215 | 216 | if not underscore_directories: 217 | raise NoDirectoriesError() 218 | 219 | # Build list of files that are in those directories 220 | replacers = [] 221 | for handler in underscore_directories: 222 | for directory, dirs, files in self.dir_helper.walk(handler.path()): 223 | for f in files: 224 | replacers.append( 225 | self.operations_factory.create_replacer( 226 | handler.relative_path(directory, f), 227 | handler.new_relative_path(directory, f) 228 | ) 229 | ) 230 | 231 | # Build list of handlers to process all files 232 | filelist = [] 233 | for root, dirs, files in self.dir_helper.walk(path): 234 | for f in files: 235 | if f.endswith(".html"): 236 | filelist.append( 237 | self.handler_factory.create_file_handler( 238 | self.file_helper.path_join(root, f), 239 | replacers, 240 | self.file_helper.open_) 241 | ) 242 | if f.endswith(".js"): 243 | filelist.append( 244 | self.handler_factory.create_file_handler( 245 | self.file_helper.path_join(root, f), 246 | [self.operations_factory.create_replacer("'_sources/'", "'sources/'")], 247 | self.file_helper.open_ 248 | ) 249 | ) 250 | 251 | return Layout(underscore_directories, filelist) 252 | 253 | def is_underscore_dir(self, path, directory): 254 | 255 | return (self.dir_helper.is_dir(self.file_helper.path_join(path, directory)) 256 | and directory.startswith("_")) 257 | 258 | 259 | 260 | def sphinx_extension(app, exception): 261 | "Wrapped up as a Sphinx Extension" 262 | 263 | # This code is sadly untestable in its current state 264 | # It would be helped if there was some function for loading extension 265 | # specific data on to the app object and the app object providing 266 | # a file-like object for writing to standard out. 267 | # The former is doable, but not officially supported (as far as I know) 268 | # so I wouldn't know where to stash the data. 269 | 270 | if app.builder.name != "html": 271 | return 272 | 273 | if not app.config.sphinx_to_github: 274 | if app.config.sphinx_to_github_verbose: 275 | print "Sphinx-to-github: Disabled, doing nothing." 276 | return 277 | 278 | if exception: 279 | if app.config.sphinx_to_github_verbose: 280 | print "Sphinx-to-github: Exception raised in main build, doing nothing." 281 | return 282 | 283 | dir_helper = DirHelper( 284 | os.path.isdir, 285 | os.listdir, 286 | os.walk, 287 | shutil.rmtree 288 | ) 289 | 290 | file_helper = FileSystemHelper( 291 | open, 292 | os.path.join, 293 | shutil.move, 294 | os.path.exists 295 | ) 296 | 297 | operations_factory = OperationsFactory() 298 | handler_factory = HandlerFactory() 299 | 300 | layout_factory = LayoutFactory( 301 | operations_factory, 302 | handler_factory, 303 | file_helper, 304 | dir_helper, 305 | app.config.sphinx_to_github_verbose, 306 | sys.stdout, 307 | force=True 308 | ) 309 | 310 | layout = layout_factory.create_layout(app.outdir) 311 | layout.process() 312 | 313 | 314 | def setup(app): 315 | "Setup function for Sphinx Extension" 316 | 317 | app.add_config_value("sphinx_to_github", True, '') 318 | app.add_config_value("sphinx_to_github_verbose", True, '') 319 | 320 | app.connect("build-finished", sphinx_extension) 321 | 322 | 323 | def main(args): 324 | 325 | usage = "usage: %prog [options] " 326 | parser = OptionParser(usage=usage) 327 | parser.add_option("-v","--verbose", action="store_true", 328 | dest="verbose", default=False, help="Provides verbose output") 329 | opts, args = parser.parse_args(args) 330 | 331 | try: 332 | path = args[0] 333 | except IndexError: 334 | sys.stderr.write( 335 | "Error - Expecting path to html directory:" 336 | "sphinx-to-github \n" 337 | ) 338 | return 339 | 340 | dir_helper = DirHelper( 341 | os.path.isdir, 342 | os.listdir, 343 | os.walk, 344 | shutil.rmtree 345 | ) 346 | 347 | file_helper = FileSystemHelper( 348 | open, 349 | os.path.join, 350 | shutil.move, 351 | os.path.exists 352 | ) 353 | 354 | operations_factory = OperationsFactory() 355 | handler_factory = HandlerFactory() 356 | 357 | layout_factory = LayoutFactory( 358 | operations_factory, 359 | handler_factory, 360 | file_helper, 361 | dir_helper, 362 | opts.verbose, 363 | sys.stdout, 364 | force=False 365 | ) 366 | 367 | try: 368 | layout = layout_factory.create_layout(path) 369 | except NoDirectoriesError: 370 | sys.stderr.write( 371 | "Error - No top level directories starting with an underscore " 372 | "were found in '%s'\n" % path 373 | ) 374 | return 375 | 376 | layout.process() 377 | 378 | 379 | 380 | if __name__ == "__main__": 381 | main(sys.argv[1:]) 382 | 383 | 384 | 385 | -------------------------------------------------------------------------------- /doc/source/throttling.rst: -------------------------------------------------------------------------------- 1 | .. _throttling: 2 | 3 | Throttling 4 | ++++++++++ 5 | 6 | Sometimes you may not want people to call a certain action many times in a 7 | short period of time. dj-webmachine allows you to throttle requests 8 | using different methods. 9 | 10 | Interval 11 | -------- 12 | 13 | This rate limiter strategy throttles the application by enforcing a 14 | minimal interval (by default, 1 second) betweeb subsequent allowed 15 | HTTP requests. 16 | 17 | In the resource object: 18 | 19 | .. code-block:: python 20 | 21 | from webmachine import Resource 22 | from webmachine.throttle import Interval 23 | 24 | class MyResource(Resource): 25 | ... 26 | 27 | def forbidden(self, req, resp): 28 | return Interval(self).allowed(req) 29 | 30 | You can throttle according to the request method too by simply checking 31 | the request instance: 32 | 33 | .. code-block:: python 34 | 35 | def forbidden(self, req, resp): 36 | if req.method == 'POST': 37 | return Interval(self).allowed(req) 38 | 39 | You can also throttle using the :ref:`route decorator `: 40 | 41 | .. code-block:: python 42 | 43 | def throttle_post(req, resp): 44 | if req.method == 'POST': 45 | return Interval(self).allowed(req) 46 | 47 | 48 | @wm.route("^$", forbbiden=throttle_post) 49 | def myres(req, resp): 50 | ... 51 | 52 | TimeWindow 53 | ---------- 54 | 55 | This rate limiter strategy throttles the application by defining a 56 | maximum number of allowed HTTP requests in a time window. 57 | 58 | 59 | Daily 60 | ~~~~~ 61 | 62 | This rate limiter strategy throttles the application by defining a 63 | maximum number of allowed HTTP requests per day (by default, 86,400 64 | requests per 24 hours, which works out to an average of 1 request per 65 | second). 66 | 67 | .. note:: 68 | 69 | This strategy doesn't use a sliding time window, but rather 70 | tracks requests per calendar day. This means that the throttling counter 71 | is reset at midnight (according to the server's local timezone) every 72 | night. 73 | 74 | 75 | .. code-block:: python 76 | 77 | from webmachine import Resource 78 | from webmachine.throttle import Daily 79 | 80 | class MyResource(Resource): 81 | ... 82 | 83 | def forbidden(self, req, resp): 84 | return Daily(self).allowed(req) 85 | 86 | Hourly 87 | ~~~~~~ 88 | 89 | 90 | This rate limiter strategy throttles the application by defining a 91 | maximum number of allowed HTTP requests per hour (by default, 3,600 92 | requests per 60 minutes, which works out to an average of 1 request per 93 | second). 94 | 95 | .. note:: 96 | 97 | Note that this strategy doesn't use a sliding time window, but rather 98 | tracks requests per distinct hour. This means that the throttling 99 | counter is reset every hour on the hour (according to the server's local 100 | timezone). 101 | 102 | .. code-block:: python 103 | 104 | from webmachine import Resource 105 | from webmachine.throttle import Hourly 106 | 107 | class MyResource(Resource): 108 | ... 109 | 110 | def forbidden(self, req, resp): 111 | return Hourly(self).allowed(req) 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /doc/source/wm.rst: -------------------------------------------------------------------------------- 1 | .. _wm: 2 | 3 | Simple Routing 4 | +++++++++++++++++++++++++++++++++++++ 5 | 6 | Combinating the power of Django and the :ref:`resources ` it's relatively easy to buid an api. The process is also eased using the WM object. dj-webmachine offer a way to create automatically resources by using the ``route`` decorator. 7 | 8 | Using this decorator, our helloworld example can be rewritten like that: 9 | 10 | .. code-block:: python 11 | 12 | 13 | from webmachine.route import wm 14 | 15 | import json 16 | @wm.route(r"^$") 17 | def hello(req, resp): 18 | return "

hello world!

" 19 | 20 | 21 | @wm.route(r"^$", provided=[("application/json", json.dumps)]) 22 | def hello_json(req, resp): 23 | return {"ok": True, "message": "hellow world"} 24 | 25 | and the urls.py: 26 | 27 | .. code-block:: python 28 | 29 | from django.conf.urls.defaults import * 30 | 31 | import webmachine 32 | 33 | webmachine.autodiscover() 34 | 35 | urlpatterns = patterns('', 36 | (r'^', include(webmachine.wm.urls)) 37 | ) 38 | 39 | The autodiscover will detect all resources modules and add then to the 40 | url dispatching. The route decorator works a little like the one in 41 | bottle_ or for that matter flask_ (though bottle was the first). 42 | 43 | This decorator works differently though. It creates full 44 | :class:`webmachine.resource.Resource` instancse registered in the wm 45 | object. So we are abble to provide all the features available in a 46 | resource: 47 | 48 | - settings which content is accepted, provided 49 | - assiciate serializers to the content types 50 | - throttling 51 | - authorization 52 | 53 | The helloworld could be written in one function: 54 | 55 | 56 | .. code-block:: python 57 | 58 | def resource_exists(req, resp): 59 | return True 60 | 61 | @wm.route(r"^hello$", 62 | provided=["text/html", ("application/json", json.dumps)], 63 | resource_exists=resource_exists 64 | 65 | ) 66 | def all_in_one(req, resp): 67 | if resp.content_type == "application/json": 68 | return {"ok": True, "message": "hellow world! All in one"} 69 | else: 70 | return "

hello world! All in one

" 71 | 72 | 73 | You can see that we set in the decorator the provided contents, we call 74 | a function ``resources_exists`` to check if this resource exists. Then 75 | in the function we return the content dependiong on the response content 76 | type depending on the HTTP requests. The response is then automatically 77 | serialized using the ``json.dumps`` function associated to the json 78 | content-type. Easy isn't it ? You can pass any :ref:`resource methods ` 79 | to the decorator. 80 | 81 | Mapping a resource 82 | ------------------ 83 | 84 | You can also map your :ref:`Resources classes` using the wm 85 | object and the method :func:`webmachine.route.WM.add_resource` 86 | 87 | If a pattern is given, the path will be 88 | ``///pattern/resource urls``. if no pattern is given the resource_name will be used. 89 | 90 | Ex for urls.py: 91 | 92 | .. code-block:: python 93 | 94 | from django.conf.urls.defaults import * 95 | 96 | import webmachine 97 | webmachine.autodiscover() 98 | 99 | urlpatterns = patterns('', 100 | (r'^wm/', include(webmachine.wm.urls)), 101 | 102 | ) 103 | 104 | and a resources.py file in one django app named hello 105 | 106 | .. code-block:: python 107 | 108 | from webmachine import Resource 109 | from webmachine import wm 110 | 111 | class Hello(Resource): 112 | 113 | def to_html(self, req, resp): 114 | return "Hello world!\n" 115 | 116 | 117 | # available at wm/hello 118 | wm.add_resource(Hello, r"^hello") 119 | 120 | # available at wm/helloworld/hello 121 | wm.add_resource(Hello) 122 | 123 | You can then access to /wm/hello and /wm/hello/hello pathes 124 | automatically. 125 | 126 | You can also override the resource path by using the Meta class: 127 | 128 | .. code-block:: python 129 | 130 | class Hello(Resource): 131 | class Meta: 132 | resource_path = "" 133 | 134 | If you set the resource path, the resource url added to the 135 | **WM** instance i 136 | ``////resource_urls`` . 137 | 138 | Custom WM instance 139 | ------------------ 140 | 141 | Sometimes you want to create custom WM instance instead to use the 142 | global one provided. For that you can import the class 143 | :class:`webmachine.route.WM`: 144 | 145 | .. autoclass:: webmachine.route.WM 146 | :members: 147 | :undoc-members: 148 | 149 | .. _bottle: http://bottle.paws.de/ 150 | .. _flask: http://flask.pocoo.org 151 | -------------------------------------------------------------------------------- /doc/sphinxtogithub.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | 3 | import optparse as op 4 | import os 5 | import sys 6 | import shutil 7 | 8 | 9 | class NoDirectoriesError(Exception): 10 | "Error thrown when no directories starting with an underscore are found" 11 | 12 | 13 | class DirHelper(object): 14 | 15 | def __init__(self, is_dir, list_dir, walk, rmtree): 16 | 17 | self.is_dir = is_dir 18 | self.list_dir = list_dir 19 | self.walk = walk 20 | self.rmtree = rmtree 21 | 22 | 23 | class FileSystemHelper(object): 24 | 25 | def __init__(self, open_, path_join, move, exists): 26 | 27 | self.open_ = open_ 28 | self.path_join = path_join 29 | self.move = move 30 | self.exists = exists 31 | 32 | 33 | class Replacer(object): 34 | "Encapsulates a simple text replace" 35 | 36 | def __init__(self, from_, to): 37 | 38 | self.from_ = from_ 39 | self.to = to 40 | 41 | def process(self, text): 42 | 43 | return text.replace(self.from_, self.to) 44 | 45 | 46 | class FileHandler(object): 47 | "Applies a series of replacements the contents of a file inplace" 48 | 49 | def __init__(self, name, replacers, opener): 50 | 51 | self.name = name 52 | self.replacers = replacers 53 | self.opener = opener 54 | 55 | def process(self): 56 | 57 | text = self.opener(self.name).read() 58 | 59 | for replacer in self.replacers: 60 | text = replacer.process(text) 61 | 62 | self.opener(self.name, "w").write(text) 63 | 64 | 65 | class Remover(object): 66 | 67 | def __init__(self, exists, remove): 68 | self.exists = exists 69 | self.remove = remove 70 | 71 | def __call__(self, name): 72 | 73 | if self.exists(name): 74 | self.remove(name) 75 | 76 | 77 | class ForceRename(object): 78 | 79 | def __init__(self, renamer, remove): 80 | 81 | self.renamer = renamer 82 | self.remove = remove 83 | 84 | def __call__(self, from_, to): 85 | 86 | self.remove(to) 87 | self.renamer(from_, to) 88 | 89 | 90 | class VerboseRename(object): 91 | 92 | def __init__(self, renamer, stream): 93 | 94 | self.renamer = renamer 95 | self.stream = stream 96 | 97 | def __call__(self, from_, to): 98 | 99 | self.stream.write( 100 | "Renaming directory '%s' -> '%s'\n" 101 | % (os.path.basename(from_), os.path.basename(to)) 102 | ) 103 | 104 | self.renamer(from_, to) 105 | 106 | 107 | class DirectoryHandler(object): 108 | "Encapsulates renaming a directory by removing its first character" 109 | 110 | def __init__(self, name, root, renamer): 111 | 112 | self.name = name 113 | self.new_name = name[1:] 114 | self.root = root + os.sep 115 | self.renamer = renamer 116 | 117 | def path(self): 118 | 119 | return os.path.join(self.root, self.name) 120 | 121 | def relative_path(self, directory, filename): 122 | 123 | path = directory.replace(self.root, "", 1) 124 | return os.path.join(path, filename) 125 | 126 | def new_relative_path(self, directory, filename): 127 | 128 | path = self.relative_path(directory, filename) 129 | return path.replace(self.name, self.new_name, 1) 130 | 131 | def process(self): 132 | 133 | from_ = os.path.join(self.root, self.name) 134 | to = os.path.join(self.root, self.new_name) 135 | self.renamer(from_, to) 136 | 137 | 138 | class HandlerFactory(object): 139 | 140 | def create_file_handler(self, name, replacers, opener): 141 | 142 | return FileHandler(name, replacers, opener) 143 | 144 | def create_dir_handler(self, name, root, renamer): 145 | 146 | return DirectoryHandler(name, root, renamer) 147 | 148 | 149 | class OperationsFactory(object): 150 | 151 | def create_force_rename(self, renamer, remover): 152 | 153 | return ForceRename(renamer, remover) 154 | 155 | def create_verbose_rename(self, renamer, stream): 156 | 157 | return VerboseRename(renamer, stream) 158 | 159 | def create_replacer(self, from_, to): 160 | 161 | return Replacer(from_, to) 162 | 163 | def create_remover(self, exists, remove): 164 | 165 | return Remover(exists, remove) 166 | 167 | 168 | class Layout(object): 169 | """ 170 | Applies a set of operations which result in the layout 171 | of a directory changing 172 | """ 173 | 174 | def __init__(self, directory_handlers, file_handlers): 175 | 176 | self.directory_handlers = directory_handlers 177 | self.file_handlers = file_handlers 178 | 179 | def process(self): 180 | 181 | for handler in self.file_handlers: 182 | handler.process() 183 | 184 | for handler in self.directory_handlers: 185 | handler.process() 186 | 187 | 188 | class LayoutFactory(object): 189 | "Creates a layout object" 190 | 191 | def __init__(self, operations_factory, handler_factory, file_helper, 192 | dir_helper, verbose, stream, force): 193 | 194 | self.operations_factory = operations_factory 195 | self.handler_factory = handler_factory 196 | 197 | self.file_helper = file_helper 198 | self.dir_helper = dir_helper 199 | 200 | self.verbose = verbose 201 | self.output_stream = stream 202 | self.force = force 203 | 204 | def create_layout(self, path): 205 | 206 | contents = self.dir_helper.list_dir(path) 207 | 208 | renamer = self.file_helper.move 209 | 210 | if self.force: 211 | remove = self.operations_factory.create_remover(self.file_helper.exists, self.dir_helper.rmtree) 212 | renamer = self.operations_factory.create_force_rename(renamer, remove) 213 | 214 | if self.verbose: 215 | renamer = self.operations_factory.create_verbose_rename(renamer, self.output_stream) 216 | 217 | # Build list of directories to process 218 | directories = [d for d in contents if self.is_underscore_dir(path, d)] 219 | underscore_directories = [ 220 | self.handler_factory.create_dir_handler(d, path, renamer) 221 | for d in directories 222 | ] 223 | 224 | if not underscore_directories: 225 | raise NoDirectoriesError() 226 | 227 | # Build list of files that are in those directories 228 | replacers = [] 229 | for handler in underscore_directories: 230 | for directory, dirs, files in self.dir_helper.walk(handler.path()): 231 | for f in files: 232 | replacers.append( 233 | self.operations_factory.create_replacer( 234 | handler.relative_path(directory, f), 235 | handler.new_relative_path(directory, f) 236 | ) 237 | ) 238 | 239 | # Build list of handlers to process all files 240 | filelist = [] 241 | for root, dirs, files in self.dir_helper.walk(path): 242 | for f in files: 243 | if f.endswith(".html"): 244 | filelist.append( 245 | self.handler_factory.create_file_handler( 246 | self.file_helper.path_join(root, f), 247 | replacers, 248 | self.file_helper.open_) 249 | ) 250 | if f.endswith(".js"): 251 | filelist.append( 252 | self.handler_factory.create_file_handler( 253 | self.file_helper.path_join(root, f), 254 | [self.operations_factory.create_replacer("'_sources/'", "'sources/'")], 255 | self.file_helper.open_ 256 | ) 257 | ) 258 | 259 | return Layout(underscore_directories, filelist) 260 | 261 | def is_underscore_dir(self, path, directory): 262 | 263 | return (self.dir_helper.is_dir(self.file_helper.path_join(path, directory)) 264 | and directory.startswith("_")) 265 | 266 | 267 | def sphinx_extension(app, exception): 268 | "Wrapped up as a Sphinx Extension" 269 | 270 | # This code is sadly untestable in its current state 271 | # It would be helped if there was some function for loading extension 272 | # specific data on to the app object and the app object providing 273 | # a file-like object for writing to standard out. 274 | # The former is doable, but not officially supported (as far as I know) 275 | # so I wouldn't know where to stash the data. 276 | 277 | if app.builder.name != "html": 278 | return 279 | 280 | if not app.config.sphinx_to_github: 281 | if app.config.sphinx_to_github_verbose: 282 | print "Sphinx-to-github: Disabled, doing nothing." 283 | return 284 | 285 | if exception: 286 | if app.config.sphinx_to_github_verbose: 287 | print "Sphinx-to-github: Exception raised in main build, doing nothing." 288 | return 289 | 290 | dir_helper = DirHelper( 291 | os.path.isdir, 292 | os.listdir, 293 | os.walk, 294 | shutil.rmtree 295 | ) 296 | 297 | file_helper = FileSystemHelper( 298 | open, 299 | os.path.join, 300 | shutil.move, 301 | os.path.exists 302 | ) 303 | 304 | operations_factory = OperationsFactory() 305 | handler_factory = HandlerFactory() 306 | 307 | layout_factory = LayoutFactory( 308 | operations_factory, 309 | handler_factory, 310 | file_helper, 311 | dir_helper, 312 | app.config.sphinx_to_github_verbose, 313 | sys.stdout, 314 | force=True 315 | ) 316 | 317 | layout = layout_factory.create_layout(app.outdir) 318 | layout.process() 319 | 320 | 321 | def setup(app): 322 | "Setup function for Sphinx Extension" 323 | 324 | app.add_config_value("sphinx_to_github", True, '') 325 | app.add_config_value("sphinx_to_github_verbose", True, '') 326 | 327 | app.connect("build-finished", sphinx_extension) 328 | 329 | 330 | def main(args): 331 | 332 | usage = "usage: %prog [options] " 333 | parser = op.OptionParser(usage=usage) 334 | parser.add_option("-v", "--verbose", action="store_true", 335 | dest="verbose", default=False, help="Provides verbose output") 336 | opts, args = parser.parse_args(args) 337 | 338 | try: 339 | path = args[0] 340 | except IndexError: 341 | sys.stderr.write( 342 | "Error - Expecting path to html directory:" 343 | "sphinx-to-github \n" 344 | ) 345 | return 346 | 347 | dir_helper = DirHelper( 348 | os.path.isdir, 349 | os.listdir, 350 | os.walk, 351 | shutil.rmtree 352 | ) 353 | 354 | file_helper = FileSystemHelper( 355 | open, 356 | os.path.join, 357 | shutil.move, 358 | os.path.exists 359 | ) 360 | 361 | operations_factory = OperationsFactory() 362 | handler_factory = HandlerFactory() 363 | 364 | layout_factory = LayoutFactory( 365 | operations_factory, 366 | handler_factory, 367 | file_helper, 368 | dir_helper, 369 | opts.verbose, 370 | sys.stdout, 371 | force=False 372 | ) 373 | 374 | try: 375 | layout = layout_factory.create_layout(path) 376 | except NoDirectoriesError: 377 | sys.stderr.write( 378 | "Error - No top level directories starting with an underscore " 379 | "were found in '%s'\n" % path 380 | ) 381 | return 382 | 383 | layout.process() 384 | 385 | if __name__ == "__main__": 386 | main(sys.argv[1:]) 387 | -------------------------------------------------------------------------------- /example/helloworld/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/dj-webmachine/77653d73de57388b712eaf50de8c32ec70c182fa/example/helloworld/__init__.py -------------------------------------------------------------------------------- /example/helloworld/hello/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/dj-webmachine/77653d73de57388b712eaf50de8c32ec70c182fa/example/helloworld/hello/__init__.py -------------------------------------------------------------------------------- /example/helloworld/hello/resource.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from webmachine import Resource 4 | from webmachine import wm 5 | 6 | class Hello(Resource): 7 | 8 | def content_types_provided(self, req, resp): 9 | return ( 10 | ("", self.to_html), 11 | ("application/json", self.to_json) 12 | ) 13 | 14 | def to_html(self, req, resp): 15 | return "Hello world!\n" 16 | 17 | def to_json(self, req, resp): 18 | return "%s\n" % json.dumps({"message": "hello world!", "ok": True}) 19 | 20 | 21 | # available at wm/hello 22 | wm.add_resource(Hello, r"^hello") 23 | 24 | 25 | # available at wm/helloworld/hello 26 | wm.add_resource(Hello) 27 | 28 | -------------------------------------------------------------------------------- /example/helloworld/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("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /example/helloworld/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for testapi 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 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 15 | 'NAME': 'test.db', # Or path to database file if using sqlite3. 16 | 'USER': '', # Not used with sqlite3. 17 | 'PASSWORD': '', # Not used with sqlite3. 18 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 19 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 20 | } 21 | } 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # On Unix systems, a value of None will cause Django to use the same 27 | # timezone as the operating system. 28 | # If running in a Windows environment this must be set to the same as your 29 | # system time zone. 30 | TIME_ZONE = 'America/Chicago' 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = 'en-us' 35 | 36 | SITE_ID = 1 37 | 38 | # If you set this to False, Django will make some optimizations so as not 39 | # to load the internationalization machinery. 40 | USE_I18N = True 41 | 42 | # If you set this to False, Django will not format dates, numbers and 43 | # calendars according to the current locale 44 | USE_L10N = True 45 | 46 | # Absolute path to the directory that holds media. 47 | # Example: "/home/media/media.lawrence.com/" 48 | MEDIA_ROOT = '' 49 | 50 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 51 | # trailing slash if there is a path component (optional in other cases). 52 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 53 | MEDIA_URL = '' 54 | 55 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 56 | # trailing slash. 57 | # Examples: "http://foo.com/media/", "/media/". 58 | ADMIN_MEDIA_PREFIX = '/media/' 59 | 60 | # Make this unique, and don't share it with anybody. 61 | SECRET_KEY = 'qawvnb9++2gek1lirm7_(e=iu(km-g8)yzqo2j&l05id@20%o*' 62 | 63 | # List of callables that know how to import templates from various sources. 64 | TEMPLATE_LOADERS = ( 65 | 'django.template.loaders.filesystem.Loader', 66 | 'django.template.loaders.app_directories.Loader', 67 | # 'django.template.loaders.eggs.Loader', 68 | ) 69 | 70 | MIDDLEWARE_CLASSES = ( 71 | 'django.middleware.common.CommonMiddleware', 72 | 'django.contrib.sessions.middleware.SessionMiddleware', 73 | 'django.middleware.csrf.CsrfViewMiddleware', 74 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 75 | 'django.contrib.messages.middleware.MessageMiddleware', 76 | ) 77 | 78 | ROOT_URLCONF = 'helloworld.urls' 79 | 80 | TEMPLATE_DIRS = ( 81 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 82 | # Always use forward slashes, even on Windows. 83 | # Don't forget to use absolute paths, not relative paths. 84 | ) 85 | 86 | INSTALLED_APPS = ( 87 | 'django.contrib.auth', 88 | 'django.contrib.contenttypes', 89 | 'django.contrib.sessions', 90 | 'django.contrib.sites', 91 | 'django.contrib.messages', 92 | # Uncomment the next line to enable the admin: 93 | # 'django.contrib.admin', 94 | # Uncomment the next line to enable admin documentation: 95 | # 'django.contrib.admindocs', 96 | 'webmachine', 97 | 'helloworld.hello', 98 | ) 99 | -------------------------------------------------------------------------------- /example/helloworld/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from helloworld.hello.resource import Hello 3 | 4 | import webmachine 5 | urlpatterns = patterns('', 6 | (r'^wm/', include(webmachine.wm.urls)), 7 | (r'^$', Hello()), 8 | 9 | ) 10 | 11 | -------------------------------------------------------------------------------- /example/helloworld2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/dj-webmachine/77653d73de57388b712eaf50de8c32ec70c182fa/example/helloworld2/__init__.py -------------------------------------------------------------------------------- /example/helloworld2/hello/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/dj-webmachine/77653d73de57388b712eaf50de8c32ec70c182fa/example/helloworld2/hello/__init__.py -------------------------------------------------------------------------------- /example/helloworld2/hello/resources.py: -------------------------------------------------------------------------------- 1 | from webmachine import wm 2 | 3 | import json 4 | @wm.route(r"^$") 5 | def hello(req, resp): 6 | return "

hello world!

" 7 | 8 | 9 | @wm.route(r"^$", provided=[("application/json", json.dumps)]) 10 | def hello_json(req, resp): 11 | return {"ok": True, "message": "hellow world"} 12 | 13 | 14 | def resource_exists(req, resp): 15 | return True 16 | 17 | @wm.route(r"^hello$", 18 | provided=["text/html", ("application/json", json.dumps)], 19 | resource_exists=resource_exists 20 | 21 | ) 22 | def all_in_one(req, resp): 23 | if resp.content_type == "application/json": 24 | return {"ok": True, "message": "hellow world! All in one"} 25 | else: 26 | return "

hello world! All in one

" 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/helloworld2/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("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /example/helloworld2/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for helloworld2 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 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 15 | 'NAME': '', # Or path to database file if using sqlite3. 16 | 'USER': '', # Not used with sqlite3. 17 | 'PASSWORD': '', # Not used with sqlite3. 18 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 19 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 20 | } 21 | } 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # On Unix systems, a value of None will cause Django to use the same 27 | # timezone as the operating system. 28 | # If running in a Windows environment this must be set to the same as your 29 | # system time zone. 30 | TIME_ZONE = 'America/Chicago' 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = 'en-us' 35 | 36 | SITE_ID = 1 37 | 38 | # If you set this to False, Django will make some optimizations so as not 39 | # to load the internationalization machinery. 40 | USE_I18N = True 41 | 42 | # If you set this to False, Django will not format dates, numbers and 43 | # calendars according to the current locale 44 | USE_L10N = True 45 | 46 | # Absolute path to the directory that holds media. 47 | # Example: "/home/media/media.lawrence.com/" 48 | MEDIA_ROOT = '' 49 | 50 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 51 | # trailing slash if there is a path component (optional in other cases). 52 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 53 | MEDIA_URL = '' 54 | 55 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 56 | # trailing slash. 57 | # Examples: "http://foo.com/media/", "/media/". 58 | ADMIN_MEDIA_PREFIX = '/media/' 59 | 60 | # Make this unique, and don't share it with anybody. 61 | SECRET_KEY = 'z6@bl1^xn9jpajhjktj3bk-*j1x9tmc_2zyndlw3-x$g2wu1z@' 62 | 63 | # List of callables that know how to import templates from various sources. 64 | TEMPLATE_LOADERS = ( 65 | 'django.template.loaders.filesystem.Loader', 66 | 'django.template.loaders.app_directories.Loader', 67 | # 'django.template.loaders.eggs.Loader', 68 | ) 69 | 70 | MIDDLEWARE_CLASSES = ( 71 | 'django.middleware.common.CommonMiddleware', 72 | 'django.contrib.sessions.middleware.SessionMiddleware', 73 | 'django.middleware.csrf.CsrfViewMiddleware', 74 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 75 | 'django.contrib.messages.middleware.MessageMiddleware', 76 | ) 77 | 78 | ROOT_URLCONF = 'helloworld2.urls' 79 | 80 | TEMPLATE_DIRS = ( 81 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 82 | # Always use forward slashes, even on Windows. 83 | # Don't forget to use absolute paths, not relative paths. 84 | ) 85 | 86 | INSTALLED_APPS = ( 87 | 'django.contrib.auth', 88 | 'django.contrib.contenttypes', 89 | 'django.contrib.sessions', 90 | 'django.contrib.sites', 91 | 'django.contrib.messages', 92 | # Uncomment the next line to enable the admin: 93 | # 'django.contrib.admin', 94 | # Uncomment the next line to enable admin documentation: 95 | # 'django.contrib.admindocs', 96 | 'webmachine', 97 | 'helloworld2.hello' 98 | ) 99 | -------------------------------------------------------------------------------- /example/helloworld2/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | import webmachine 4 | 5 | webmachine.autodiscover() 6 | print len(webmachine.wm.routes) 7 | 8 | urlpatterns = patterns('', 9 | (r'^', include(webmachine.wm.urls)) 10 | ) 11 | -------------------------------------------------------------------------------- /example/testoauth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/dj-webmachine/77653d73de57388b712eaf50de8c32ec70c182fa/example/testoauth/__init__.py -------------------------------------------------------------------------------- /example/testoauth/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("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /example/testoauth/protected/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/dj-webmachine/77653d73de57388b712eaf50de8c32ec70c182fa/example/testoauth/protected/__init__.py -------------------------------------------------------------------------------- /example/testoauth/protected/resource.py: -------------------------------------------------------------------------------- 1 | from webmachine import Resource 2 | from webmachine.auth.oauth import Oauth 3 | 4 | class Protected(Resource): 5 | 6 | def to_html(self, req, resp): 7 | return "

I'm protected you know.

" 8 | 9 | def is_authorized(self, req, resp): 10 | return Oauth().authorized(req, resp) 11 | -------------------------------------------------------------------------------- /example/testoauth/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for oauth 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 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 15 | 'NAME': 'oauth.db', # Or path to database file if using sqlite3. 16 | 'USER': '', # Not used with sqlite3. 17 | 'PASSWORD': '', # Not used with sqlite3. 18 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 19 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 20 | } 21 | } 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # On Unix systems, a value of None will cause Django to use the same 27 | # timezone as the operating system. 28 | # If running in a Windows environment this must be set to the same as your 29 | # system time zone. 30 | TIME_ZONE = 'Europe/Paris' 31 | 32 | # Language code for this installation. All choices can be found here: 33 | # http://www.i18nguy.com/unicode/language-identifiers.html 34 | LANGUAGE_CODE = 'en-us' 35 | 36 | SITE_ID = 1 37 | 38 | # If you set this to False, Django will make some optimizations so as not 39 | # to load the internationalization machinery. 40 | USE_I18N = True 41 | 42 | # If you set this to False, Django will not format dates, numbers and 43 | # calendars according to the current locale 44 | USE_L10N = True 45 | 46 | # Absolute path to the directory that holds media. 47 | # Example: "/home/media/media.lawrence.com/" 48 | MEDIA_ROOT = '' 49 | 50 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 51 | # trailing slash if there is a path component (optional in other cases). 52 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 53 | MEDIA_URL = '' 54 | 55 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 56 | # trailing slash. 57 | # Examples: "http://foo.com/media/", "/media/". 58 | ADMIN_MEDIA_PREFIX = '/media/' 59 | 60 | # Make this unique, and don't share it with anybody. 61 | SECRET_KEY = '#i=e163$15)prr-_mpo!po085%jtan0y0%yd8gx++wz0fy(qg%' 62 | 63 | # List of callables that know how to import templates from various sources. 64 | TEMPLATE_LOADERS = ( 65 | 'django.template.loaders.filesystem.Loader', 66 | 'django.template.loaders.app_directories.Loader', 67 | # 'django.template.loaders.eggs.Loader', 68 | ) 69 | 70 | MIDDLEWARE_CLASSES = ( 71 | 'django.middleware.common.CommonMiddleware', 72 | 'django.contrib.sessions.middleware.SessionMiddleware', 73 | 'django.middleware.csrf.CsrfViewMiddleware', 74 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 75 | 'django.contrib.messages.middleware.MessageMiddleware', 76 | ) 77 | 78 | ROOT_URLCONF = 'testoauth.urls' 79 | 80 | TEMPLATE_DIRS = ( 81 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 82 | # Always use forward slashes, even on Windows. 83 | # Don't forget to use absolute paths, not relative paths. 84 | ) 85 | 86 | INSTALLED_APPS = ( 87 | 'django.contrib.auth', 88 | 'django.contrib.contenttypes', 89 | 'django.contrib.sessions', 90 | 'django.contrib.sites', 91 | 'django.contrib.messages', 92 | # Uncomment the next line to enable the admin: 93 | # 'django.contrib.admin', 94 | # Uncomment the next line to enable admin documentation: 95 | # 'django.contrib.admindocs', 96 | 'webmachine', 97 | 'testoauth.protected' 98 | ) 99 | -------------------------------------------------------------------------------- /example/testoauth/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from webmachine.auth import oauth_res 3 | 4 | from testoauth.protected.resource import Protected 5 | 6 | # Uncomment the next two lines to enable the admin: 7 | # from django.contrib import admin 8 | # admin.autodiscover() 9 | 10 | 11 | urlpatterns = patterns('', 12 | # Example: 13 | # (r'^oauth/', include('oauth.foo.urls')), 14 | 15 | # Uncomment the admin/doc line below to enable admin documentation: 16 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 17 | 18 | # Uncomment the next line to enable the admin: 19 | # (r'^admin/', include(admin.site.urls)), 20 | 21 | (r'^auth/', include(oauth_res.OauthResource().get_urls())), 22 | (r'$^', Protected()), 23 | ) 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This file is part of dj-webmachine released under the Apache 2 license. 5 | # See the NOTICE for more information. 6 | 7 | from distutils.command.install_data import install_data 8 | import os 9 | import sys 10 | 11 | if not hasattr(sys, 'version_info') or sys.version_info < (2, 5, 0, 'final'): 12 | raise SystemExit("Compono requires Python 2.5 or later.") 13 | 14 | from setuptools import setup, find_packages 15 | 16 | from webmachine import __version__ 17 | 18 | 19 | data_files = [] 20 | for root in ('webmachine/media', 'webmachine/templates'): 21 | for dir, dirs, files in os.walk(root): 22 | dirs[:] = [x for x in dirs if not x.startswith('.')] 23 | files = [x for x in files if not x.startswith('.')] 24 | data_files.append((os.path.join('webmachine', dir), 25 | [os.path.join(dir, file_) for file_ in files])) 26 | 27 | class install_package_data(install_data): 28 | def finalize_options(self): 29 | self.set_undefined_options('install', 30 | ('install_lib', 'install_dir')) 31 | install_data.finalize_options(self) 32 | cmdclass = {'install_data': install_package_data } 33 | 34 | setup( 35 | name = 'dj-webmachine', 36 | version = __version__, 37 | description = 'Minimal Django Resource framework.', 38 | long_description = file( 39 | os.path.join( 40 | os.path.dirname(__file__), 41 | 'README.rst' 42 | ) 43 | ).read(), 44 | author = 'Benoit Chesneau', 45 | author_email = 'benoitc@e-engura.org', 46 | license = 'BSD', 47 | url = 'http://github.com/benoitc/dj-webmachine', 48 | classifiers = [ 49 | 'License :: OSI Approved :: MIT License', 50 | 'Intended Audience :: Developers', 51 | 'Intended Audience :: System Administrators', 52 | 'Development Status :: 4 - Beta', 53 | 'Programming Language :: Python', 54 | 'Operating System :: OS Independent', 55 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 56 | 'Topic :: Software Development', 57 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 58 | 59 | ], 60 | 61 | zip_safe = False, 62 | packages = find_packages(), 63 | include_package_data = True, 64 | data_files = data_files, 65 | cmdclass=cmdclass, 66 | install_requires = [ 67 | 'setuptools', 68 | 'webob' 69 | ], 70 | 71 | test_suite = 'nose.collector', 72 | 73 | ) 74 | -------------------------------------------------------------------------------- /webmachine/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | 7 | version_info = (0, 3, 0) 8 | __version__ = ".".join(map(str, version_info)) 9 | 10 | 11 | try: 12 | from webmachine.route import WM, wm 13 | from webmachine.resource import Resource 14 | except ImportError: 15 | import traceback 16 | traceback.print_exc() 17 | 18 | def autodiscover(): 19 | """ 20 | Auto-discover INSTALLED_APPS resource.py modules and fail silently when 21 | not present. This forces an import on them to register any resource bits they 22 | may want. 23 | """ 24 | 25 | from django.conf import settings 26 | from django.utils.importlib import import_module 27 | from django.utils.module_loading import module_has_submodule 28 | 29 | for app in settings.INSTALLED_APPS: 30 | mod = import_module(app) 31 | # Attempt to import the app's resource module. 32 | try: 33 | import_module('%s.resources' % app) 34 | except: 35 | if module_has_submodule(mod, 'resources'): 36 | raise 37 | -------------------------------------------------------------------------------- /webmachine/auth/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from webmachine.auth.base import Auth, BasicAuth 7 | -------------------------------------------------------------------------------- /webmachine/auth/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import binascii 7 | 8 | from django.contrib.auth import authenticate 9 | from django.contrib.auth.models import AnonymousUser 10 | 11 | from webmachine.exc import HTTPClientError 12 | 13 | class Auth(object): 14 | 15 | def authorized(self, request): 16 | return True 17 | 18 | class BasicAuth(Auth): 19 | 20 | def __init__(self, func=authenticate, realm="API"): 21 | """ 22 | :attr func: authentification function. By default it's the 23 | :func:`django.contrib.auth.authenticate` function. 24 | :attr realm: string, the authentification realm 25 | """ 26 | 27 | self.func = func 28 | self.realm = realm 29 | 30 | def authorized(self, req, resp): 31 | auth_str = req.META.get("HTTP_AUTHORIZATION") 32 | if not auth_str: 33 | return 'Basic realm="%s"' % self.realm 34 | 35 | try: 36 | (meth, auth) = auth_str.split(" ", 1) 37 | if meth.lower() != "basic": 38 | # bad method 39 | return False 40 | auth1 = auth.strip().decode('base64') 41 | (user, pwd) = auth1.split(":", 1) 42 | except (ValueError, binascii.Error): 43 | raise HTTPClientError() 44 | 45 | req.user = self.func(username=user, password=pwd) 46 | if not req.user: 47 | req.user = AnonymousUser() 48 | return 'Basic realm="%s"' % self.realm 49 | return True 50 | 51 | -------------------------------------------------------------------------------- /webmachine/auth/oauth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from django.core.exceptions import ImproperlyConfigured 7 | from django.conf import settings 8 | from django.utils.importlib import import_module 9 | 10 | try: 11 | from restkit import oauth2 12 | except ImportError: 13 | raise ImportError("restkit>=3.0.2 package is needed for auth.") 14 | 15 | from webmachine.auth.base import Auth 16 | from webmachine.util.const import TOKEN_REQUEST, TOKEN_ACCESS 17 | 18 | 19 | def load_oauth_datastore(): 20 | datastore = getattr(settings, 'OAUTH_DATASTORE', 21 | 'webmachine.auth.oauth_store.DataStore') 22 | i = datastore.rfind('.') 23 | module, clsname = datastore[:i], datastore[i+1:] 24 | try: 25 | mod = import_module(module) 26 | except ImportError: 27 | raise ImproperlyConfigured("oauth datastore module '%s' isn't valid" % module) 28 | 29 | try: 30 | cls = getattr(mod, clsname) 31 | except AttributeError: 32 | raise ImproperlyConfigured("oauth datastore '%s' doesn't exist in '%s' module" % (clsname, module)) 33 | return cls 34 | 35 | 36 | class OAuthServer(oauth2.Server): 37 | 38 | def __init__(self, datastore): 39 | self.datastore = datastore 40 | super(OAuthServer, self).__init__() 41 | 42 | def fetch_request_token(self, oauth_request): 43 | """Processes a request_token request and returns the 44 | request token on success. 45 | """ 46 | try: 47 | # Get the request token for authorization. 48 | token = self._get_token(oauth_request, TOKEN_REQUEST) 49 | except oauth2.Error: 50 | # No token required for the initial token request. 51 | timestamp = self._get_timestamp(oauth_request) 52 | version = self._get_version(oauth_request) 53 | consumer = self._get_consumer(oauth_request) 54 | try: 55 | callback = self.get_callback(oauth_request) 56 | except oauth2.Error: 57 | callback = None # 1.0, no callback specified. 58 | 59 | #hack 60 | 61 | self._check_signature(oauth_request, consumer, None) 62 | # Fetch a new token. 63 | token = self.datastore.fetch_request_token(consumer, 64 | callback, timestamp) 65 | return token 66 | 67 | def fetch_access_token(self, oauth_request): 68 | """Processes an access_token request and returns the 69 | access token on success. 70 | """ 71 | timestamp = self._get_timestamp(oauth_request) 72 | version = self._get_version(oauth_request) 73 | consumer = self._get_consumer(oauth_request) 74 | try: 75 | verifier = self._get_verifier(oauth_request) 76 | except oauth2.Error: 77 | verifier = None 78 | # Get the request token. 79 | token = self._get_token(oauth_request, TOKEN_REQUEST) 80 | self._check_signature(oauth_request, consumer, token) 81 | new_token = self.datastore.fetch_access_token(consumer, token, 82 | verifier, timestamp) 83 | return new_token 84 | 85 | def verify_request(self, oauth_request): 86 | consumer = self._get_consumer(oauth_request) 87 | token = self._get_token(oauth_request, TOKEN_ACCESS) 88 | parameters = super(OAuthServer, self).verify_request(oauth_request, 89 | consumer, token) 90 | return consumer, token, parameters 91 | 92 | def authorize_token(self, token, user): 93 | """Authorize a request token.""" 94 | return self.datastore.authorize_request_token(token, user) 95 | 96 | def get_callback(self, oauth_request): 97 | """Get the callback URL.""" 98 | return oauth_request.get_parameter('oauth_callback') 99 | 100 | def _get_consumer(self, oauth_request): 101 | consumer_key = oauth_request.get_parameter('oauth_consumer_key') 102 | consumer = self.datastore.lookup_consumer(consumer_key) 103 | if not consumer: 104 | raise oauth2.Error('Invalid consumer.') 105 | return consumer 106 | 107 | def _get_token(self, oauth_request, token_type=TOKEN_ACCESS): 108 | """Try to find the token for the provided request token key.""" 109 | token_field = oauth_request.get_parameter('oauth_token') 110 | token = self.datastore.lookup_token(token_type, token_field) 111 | if not token: 112 | raise oauth2.Error('Invalid %s token: %s' % (token_type, token_field)) 113 | return token 114 | 115 | def _check_nonce(self, consumer, token, nonce): 116 | """Verify that the nonce is uniqueish.""" 117 | nonce = self.datastore.lookup_nonce(consumer, token, nonce) 118 | if nonce: 119 | raise oauth2.Error('Nonce already used: %s' % str(nonce)) 120 | 121 | def _get_timestamp(self, oauth_request): 122 | return int(oauth_request.get_parameter('oauth_timestamp')) 123 | 124 | 125 | class Oauth(Auth): 126 | 127 | def __init__(self, realm="OAuth"): 128 | oauth_datastore = load_oauth_datastore() 129 | self.realm = realm 130 | self.oauth_server = OAuthServer(oauth_datastore()) 131 | self.oauth_server.add_signature_method(oauth2.SignatureMethod_PLAINTEXT()) 132 | self.oauth_server.add_signature_method(oauth2.SignatureMethod_HMAC_SHA1()) 133 | 134 | def authorized(self, req, resp): 135 | params = {} 136 | headers = {} 137 | 138 | if req.method == "POST": 139 | params = req.REQUEST.items() 140 | 141 | if 'HTTP_AUTHORIZATION' in req.META: 142 | headers['Authorization'] = req.META.get('HTTP_AUTHORIZATION') 143 | 144 | 145 | oauth_request = oauth2.Request.from_request(req.method, 146 | req.build_absolute_uri(), headers=headers, 147 | parameters=params, 148 | query_string=req.META.get('QUERY_STRING')) 149 | 150 | if not oauth_request: 151 | return 'OAuth realm="%s"' % self.realm 152 | 153 | try: 154 | consumer, token, params = self.oauth_server.verify_request(oauth_request) 155 | except oauth2.Error, err: 156 | resp.content = str(err) 157 | return 'OAuth realm="%s"' % self.realm 158 | 159 | req.user = consumer.user 160 | return True 161 | -------------------------------------------------------------------------------- /webmachine/auth/oauth_res.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from django.template import loader, RequestContext 7 | from django.utils.encoding import iri_to_uri 8 | 9 | try: 10 | from restkit import oauth2 11 | except ImportError: 12 | raise ImportError("restkit>=3.0.2 package is needed for auth.") 13 | 14 | from webmachine.auth.oauth import OAuthServer, load_oauth_datastore 15 | from webmachine.forms import OAuthAuthenticationForm 16 | from webmachine.resource import Resource 17 | 18 | 19 | class OauthResource(Resource): 20 | 21 | def __init__(self, realm='OAuth', 22 | auth_template='webmachine/authorize_token.html', 23 | auth_form=OAuthAuthenticationForm): 24 | 25 | self.auth_template = auth_template 26 | self.auth_form = auth_form 27 | self.realm = realm 28 | 29 | oauth_datastore = load_oauth_datastore() 30 | self.oauth_server = OAuthServer(oauth_datastore()) 31 | self.oauth_server.add_signature_method(oauth2.SignatureMethod_PLAINTEXT()) 32 | self.oauth_server.add_signature_method(oauth2.SignatureMethod_HMAC_SHA1()) 33 | 34 | def allowed_methods(self, req, resp): 35 | return ["GET", "HEAD", "POST"] 36 | 37 | def oauth_authorize(self, req, resp): 38 | try: 39 | token = self.oauth_server.fetch_request_token(req.oauth_request) 40 | except oauth2.Error, err: 41 | return self.auth_error(req, resp, err) 42 | 43 | try: 44 | callback = self.auth_server.get_callback(req.oauth_request) 45 | except: 46 | callback = None 47 | 48 | if req.method == "GET": 49 | params = req.oauth_request.get_normalized_parameters() 50 | form = self.auth_form(initial={ 51 | 'oauth_token': token.key, 52 | 'oauth_callback': token.get_callback_url() or callback, 53 | }) 54 | resp.content = loader.render_to_string(self.auth_template, 55 | {'form': form}, RequestContext(req)) 56 | 57 | elif req.method == "POST": 58 | 59 | try: 60 | form = self.auth_form(req.POST) 61 | if form.is_valid(): 62 | token = self.oauth_server.authorize_token(token, req.user) 63 | args = '?'+token.to_string(only_key=True) 64 | else: 65 | args = '?error=%s' % 'Access not granted by user.' 66 | if not callback: 67 | resp.content = 'Access not granted by user.' 68 | 69 | if not callback: 70 | return True 71 | 72 | resp.redirect_to = iri_to_uri("%s%s" % (callback, args)) 73 | except oauth2.Error, err: 74 | return self.oauth_error(req, resp, err) 75 | return True 76 | 77 | def oauth_access_token(self, req, resp): 78 | try: 79 | token = self.oauth_server.fetch_access_token(req.oauth_request) 80 | if not token: 81 | return False 82 | resp.content = token.to_string() 83 | except oauth2.Error, err: 84 | return self.oauth_error(req, resp, err) 85 | return True 86 | 87 | def oauth_request_token(self, req, resp): 88 | try: 89 | token = self.oauth_server.fetch_request_token(req.oauth_request) 90 | if not token: 91 | return False 92 | resp.content = token.to_string() 93 | except oauth2.Error, err: 94 | return self.oauth_error(req, resp, err) 95 | return True 96 | 97 | def oauth_error(self, req, resp, err): 98 | resp.content = str(err) 99 | return 'OAuth realm="%s"' % self.realm 100 | 101 | def oauth_resp(self, req, resp): 102 | return resp.content 103 | 104 | def content_types_provided(self, req, resp): 105 | return [("", self.oauth_resp)] 106 | 107 | def process_post(self, res, resp): 108 | # we already processed POST 109 | return True 110 | 111 | def created_location(self, req, resp): 112 | try: 113 | return resp.redirect_to 114 | except AttributeError: 115 | return False 116 | 117 | def is_authorized(self, req, resp): 118 | func = getattr(self, "oauth_%s" % req.oauth_action) 119 | return func(req, resp) 120 | 121 | def malformed_request(self, req, resp): 122 | params = {} 123 | headers = {} 124 | 125 | if req.method == "POST": 126 | params = dict(req.REQUEST.items()) 127 | 128 | if 'HTTP_AUTHORIZATION' in req.META: 129 | headers['Authorization'] = req.META.get('HTTP_AUTHORIZATION') 130 | 131 | oauth_request = oauth2.Request.from_request(req.method, 132 | req.build_absolute_uri(), headers=headers, 133 | parameters=params, 134 | query_string=req.META.get('QUERY_STRING')) 135 | 136 | if not oauth_request: 137 | 138 | return True 139 | 140 | req.oauth_request = oauth_request 141 | return False 142 | 143 | def ping(self, req, resp): 144 | action = req.url_kwargs.get("action") 145 | if not action or action not in ("authorize", "access_token", 146 | "request_token"): 147 | return False 148 | 149 | req.oauth_action = action 150 | 151 | return True 152 | 153 | def get_urls(self): 154 | from django.conf.urls.defaults import patterns, url 155 | urlpatterns = patterns('', 156 | url(r'^authorize$', self, 157 | kwargs={"action": "authorize"}, 158 | name="oauth_authorize"), 159 | url(r'^access_token$', self, 160 | kwargs={"action": "access_token"}, 161 | name="oauth_access_token"), 162 | url(r'^request_token$', self, 163 | kwargs= {"action": "request_token"}, 164 | name="oauth_request_token"), 165 | ) 166 | return urlpatterns 167 | 168 | urls = property(get_urls) 169 | -------------------------------------------------------------------------------- /webmachine/auth/oauth_store.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from django.contrib.auth.models import AnonymousUser 7 | 8 | from webmachine.models import Nonce, Consumer, Token 9 | from webmachine.util import generate_random 10 | from webmachine.util.const import VERIFIER_SIZE, TOKEN_REQUEST, TOKEN_ACCESS 11 | 12 | 13 | class OAuthDataStore(object): 14 | """A database abstraction used to lookup consumers and tokens.""" 15 | 16 | def lookup_consumer(self, key): 17 | """-> OAuthConsumer.""" 18 | raise NotImplementedError 19 | 20 | def lookup_token(self, token_type, key): 21 | """-> OAuthToken.""" 22 | raise NotImplementedError 23 | 24 | def lookup_nonce(self, oauth_consumer, oauth_token, nonce): 25 | """-> OAuthToken.""" 26 | raise NotImplementedError 27 | 28 | def fetch_request_token(self, oauth_consumer, oauth_callback, 29 | oauth_timestamp): 30 | """-> OAuthToken.""" 31 | raise NotImplementedError 32 | 33 | def fetch_access_token(self, oauth_consumer, oauth_token, 34 | oauth_verifier, oauth_timestamp): 35 | """-> OAuthToken.""" 36 | raise NotImplementedError 37 | 38 | def authorize_request_token(self, oauth_token, user): 39 | """-> OAuthToken.""" 40 | raise NotImplementedError 41 | 42 | 43 | class DataStore(OAuthDataStore): 44 | 45 | def lookup_consumer(self, key): 46 | try: 47 | self.consumer = Consumer.objects.get(key=key) 48 | except Consumer.DoesNotExist: 49 | return None 50 | return self.consumer 51 | 52 | def lookup_token(self, token_type, key): 53 | try: 54 | self.request_token = Token.objects.get( 55 | token_type=token_type, 56 | key=key) 57 | except Consumer.DoesNotExist: 58 | return None 59 | return self.request_token 60 | 61 | def lookup_nonce(self, consumer, token, nonce): 62 | if not token: 63 | return 64 | 65 | nonce, created = Nonce.objects.get_or_create( 66 | consumer_key=consumer.key, 67 | token_key=token.key, 68 | nonce=nonce) 69 | 70 | if created: 71 | return None 72 | return nonce 73 | 74 | def fetch_request_token(self, consumer, callback, timestamp): 75 | if consumer.key == self.consumer.key: 76 | request_token = Token.objects.create_token( 77 | consumer=self.consumer, 78 | token_type=TOKEN_REQUEST, 79 | timestamp=timestamp) 80 | 81 | if callback: 82 | self.request_token.set_callback(callback) 83 | 84 | self.request_token = request_token 85 | return request_token 86 | return None 87 | 88 | def fetch_access_token(self, consumer, token, verifier, timestamp): 89 | if consumer.key == self.consumer.key \ 90 | and token.key == self.request_token.key \ 91 | and self.request_token.is_approved: 92 | if (self.request_token.callback_confirmed \ 93 | and verifier == self.request_token.verifier) \ 94 | or not self.request_token.callback_confirmed: 95 | 96 | self.access_token = Token.objects.create_token( 97 | consumer=self.consumer, 98 | token_type=TOKEN_ACCESS, 99 | timestamp=timestamp, 100 | user=self.request_token.user) 101 | return self.access_token 102 | return None 103 | 104 | def authorize_request_token(self, oauth_token, user): 105 | if oauth_token.key == self.request_token.key: 106 | # authorize the request token in the store 107 | self.request_token.is_approved = True 108 | if not isinstance(user, AnonymousUser): 109 | self.request_token.user = user 110 | self.request_token.verifier = generate_random(VERIFIER_SIZE) 111 | self.request_token.save() 112 | return self.request_token 113 | return None 114 | -------------------------------------------------------------------------------- /webmachine/decisions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import datetime 7 | 8 | from webob.datetime_utils import UTC 9 | import webmachine.exc 10 | 11 | def b03(res, req, resp): 12 | "Options?" 13 | if req.method == 'OPTIONS': 14 | for (header, value) in res.options(req, resp): 15 | resp[header] = value 16 | return True 17 | return False 18 | 19 | def b04(res, req, resp): 20 | "Request entity too large?" 21 | return not res.valid_entity_length(req, resp) 22 | 23 | def b05(res, req, resp): 24 | "Unknown Content-Type?" 25 | return not res.known_content_type(req, resp) 26 | 27 | def b06(res, req, resp): 28 | "Unknown or unsupported Content-* header?" 29 | return not res.valid_content_headers(req, resp) 30 | 31 | def b07(res, req, resp): 32 | "Forbidden?" 33 | return res.forbidden(req, resp) 34 | 35 | def b08(res, req, resp): 36 | "Authorized?" 37 | auth = res.is_authorized(req, resp) 38 | if auth is True: 39 | return True 40 | elif isinstance(auth, basestring): 41 | resp["WWW-Authenticate"] = auth 42 | return False 43 | 44 | def b09(res, req, resp): 45 | "Malformed?" 46 | return res.malformed_request(req, resp) 47 | 48 | def b10(res, req, resp): 49 | "Is method allowed?" 50 | if req.method in res.allowed_methods(req, resp): 51 | return True 52 | return False 53 | 54 | def b11(res, req, resp): 55 | "URI too long?" 56 | return res.uri_too_long(req, resp) 57 | 58 | def b12(res, req, resp): 59 | "Known method?" 60 | return req.method in res.known_methods(req, resp) 61 | 62 | def b13(res, req, resp): 63 | "Service available?" 64 | return res.ping(req, resp) and res.service_available(req, resp) 65 | 66 | def c03(res, req, resp): 67 | "Accept exists?" 68 | return "HTTP_ACCEPT" in req.META 69 | 70 | def c04(res, req, resp): 71 | "Acceptable media type available?" 72 | ctypes = [ctype for (ctype, func) in res.content_types_provided(req, resp)] 73 | ctype = req.accept.best_match(ctypes) 74 | if ctype is None: 75 | return False 76 | resp.content_type = ctype 77 | return True 78 | 79 | def d04(res, req, resp): 80 | "Accept-Language exists?" 81 | return "HTTP_ACCEPT_LANGUAGE" in req.META 82 | 83 | def d05(res, req, resp): 84 | "Accept-Language available?" 85 | langs = res.languages_provided(req, resp) 86 | if langs is not None: 87 | lang = req.accept_language.best_match(langs) 88 | if lang is None: 89 | return False 90 | resp.content_language = lang 91 | return True 92 | 93 | def e05(res, req, resp): 94 | "Accept-Charset exists?" 95 | return "HTTP_ACCEPT_CHARSET" in req.META 96 | 97 | def e06(res, req, resp): 98 | "Acceptable charset available?" 99 | charsets = res.charsets_provided(req, resp) 100 | if charsets is not None: 101 | charset = req.accept_charset.best_match(charsets) 102 | if charset is None: 103 | return False 104 | resp._charset = charset 105 | return True 106 | 107 | def f06(res, req, resp): 108 | "Accept-Encoding exists?" 109 | return "HTTP_ACCEPT_ENCODING" in req.META 110 | 111 | def f07(res, req, resp): 112 | "Acceptable encoding available?" 113 | encodings = res.encodings_provided(req, resp) 114 | if encodings is not None: 115 | encodings = [enc for (enc, func) in encodings] 116 | enc = req.accept_encoding.best_match(encodings) 117 | if enc is None: 118 | return False 119 | resp.content_encoding = enc 120 | return True 121 | 122 | def g07(res, req, resp): 123 | "Resource exists?" 124 | 125 | # Set variances now that conneg is done 126 | hdr = [] 127 | if len(res.content_types_provided(req, resp) or []) > 1: 128 | hdr.append("Accept") 129 | if len(res.charsets_provided(req, resp) or []) > 1: 130 | hdr.append("Accept-Charset") 131 | if len(res.encodings_provided(req, resp) or []) > 1: 132 | hdr.append("Accept-Encoding") 133 | if len(res.languages_provided(req, resp) or []) > 1: 134 | hdr.append("Accept-Language") 135 | hdr.extend(res.variances(req, resp)) 136 | resp.vary = hdr 137 | 138 | return res.resource_exists(req, resp) 139 | 140 | def g08(res, req, resp): 141 | "If-Match exists?" 142 | return "HTTP_IF_MATCH" in req.META 143 | 144 | def g09(res, req, resp): 145 | "If-Match: * exists?" 146 | return '*' in req.if_match 147 | 148 | def g11(res, req, resp): 149 | "Etag in If-Match?" 150 | return res.generate_etag(req, resp) in req.if_match 151 | 152 | def h07(res, req, resp): 153 | "If-Match: * exists?" 154 | # Need to recheck that if-match was an actual header 155 | # because WebOb is says that '*' will match no header. 156 | return 'HTTP_IF_MATCH' in req.META and '*' in req.if_match 157 | 158 | def h10(res, req, resp): 159 | "If-Unmodified-Since exists?" 160 | return "HTTP_IF_MODIFIED_SINCE" in req.META 161 | 162 | def h11(res, req, resp): 163 | "If-Unmodified-Since is a valid date?" 164 | return req.if_unmodified_since is not None 165 | 166 | def h12(res, req, resp): 167 | "Last-Modified > If-Unmodified-Since?" 168 | if not req.if_unmodified_since: 169 | return True 170 | 171 | resp.last_modified = res.last_modified(req, resp) 172 | return resp.last_modified > req.if_unmodified_since 173 | 174 | def i04(res, req, resp): 175 | "Apply to a different URI?" 176 | uri = res.moved_permanently(req, resp) 177 | if not uri: 178 | return False 179 | resp.location = uri 180 | return True 181 | 182 | def i07(res, req, resp): 183 | "PUT?" 184 | return req.method == "PUT" 185 | 186 | def i12(res, req, resp): 187 | "If-None-Match exists?" 188 | return "HTTP_IF_NONE_MATCH" in req.META 189 | 190 | def i13(res, req, resp): 191 | "If-None-Match: * exists?" 192 | return '*' in req.if_none_match 193 | 194 | def j18(res, req, resp): 195 | "GET/HEAD?" 196 | return req.method in ["GET", "HEAD"] 197 | 198 | def k05(res, req, resp): 199 | "Resource moved permanently?" 200 | uri = res.moved_permanently(req, resp) 201 | if not uri: 202 | return False 203 | resp.location = uri 204 | return True 205 | 206 | def k07(res, req, resp): 207 | "Resource previously existed?" 208 | return res.previously_existed(req, resp) 209 | 210 | def k13(res, req, resp): 211 | "Etag in If-None-Match?" 212 | resp.etag = res.generate_etag(req, resp) 213 | return resp.etag in req.if_none_match 214 | 215 | def l05(res, req, resp): 216 | "Resource moved temporarily?" 217 | uri = res.moved_temporarily(req, resp) 218 | if not uri: 219 | return False 220 | resp.location = uri 221 | return True 222 | 223 | def l07(res, req, resp): 224 | "POST?" 225 | return req.method == "POST" 226 | 227 | def l13(res, req, resp): 228 | "If-Modified-Since exists?" 229 | return "HTTP_IF_MODIFIED_SINCE" in req.META 230 | 231 | def l14(res, req, resp): 232 | "If-Modified-Since is a valid date?" 233 | return req.if_modified_since is not None 234 | 235 | def l15(res, req, resp): 236 | "If-Modified-Since > Now?" 237 | return req.if_modified_since > datetime.datetime.now(UTC) 238 | 239 | def l17(res, req, resp): 240 | "Last-Modified > If-Modified-Since?" 241 | resp.last_modified = res.last_modified(req, resp) 242 | if not (req.if_modified_since and resp.last_modified): 243 | return True 244 | return resp.last_modified > req.if_modified_since 245 | 246 | def m05(res, req, resp): 247 | "POST?" 248 | return req.method == "POST" 249 | 250 | def m07(res, req, resp): 251 | "Server permits POST to missing resource?" 252 | return res.allow_missing_post(req, resp) 253 | 254 | def m16(res, req, resp): 255 | "DELETE?" 256 | return req.method == "DELETE" 257 | 258 | def m20(res, req, resp): 259 | """Delete enacted immediayly? 260 | Also where DELETE is forced.""" 261 | return res.delete_resource(req, resp) 262 | 263 | def m20b(res, req, resp): 264 | """ Delete completed """ 265 | return res.delete_completed(req, resp) 266 | 267 | def n05(res, req, resp): 268 | "Server permits POST to missing resource?" 269 | return res.allow_missing_post(req, resp) 270 | 271 | def n11(res, req, resp): 272 | "Redirect?" 273 | if res.post_is_create(req, resp): 274 | handle_request_body(res, req, resp) 275 | else: 276 | if not res.process_post(req, resp): 277 | raise webmachine.exc.HTTPInternalServerError("Failed to process POST.") 278 | return False 279 | resp.location = res.created_location(req, resp) 280 | if resp.location: 281 | return True 282 | return False 283 | 284 | 285 | def n16(res, req, resp): 286 | "POST?" 287 | return req.method == "POST" 288 | 289 | def o14(res, req, resp): 290 | "Is conflict?" 291 | if not res.is_conflict(req, resp): 292 | handle_response_body(res, req, resp) 293 | return False 294 | return True 295 | 296 | def o16(res, req, resp): 297 | "PUT?" 298 | return req.method == "PUT" 299 | 300 | def o18(res, req, resp): 301 | "Multiple representations? (Build GET/HEAD body)" 302 | if req.method not in ["GET", "HEAD"]: 303 | return res.multiple_choices(req, resp) 304 | 305 | handle_response_body(res, req, resp) 306 | return res.multiple_choices(req, resp) 307 | 308 | def o20(res, req, resp): 309 | "Response includes entity?" 310 | return bool(resp._container) 311 | 312 | def p03(res, req, resp): 313 | "Conflict?" 314 | if res.is_conflict(req, resp): 315 | return True 316 | 317 | handle_request_body(res, req, resp) 318 | return False 319 | 320 | def p11(res, req, resp): 321 | "New resource?" 322 | if not resp.location: 323 | return False 324 | return True 325 | 326 | def first_match(func, req, resp, expect): 327 | for (key, value) in func(req, resp): 328 | if key == expect: 329 | return value 330 | return None 331 | 332 | def handle_request_body(res, req, resp): 333 | ctype = req.content_type or "application/octet-stream" 334 | mtype = ctype.split(";", 1)[0] 335 | 336 | func = first_match(res.content_types_accepted, req, resp, mtype) 337 | if func is None: 338 | raise webmachine.exc.HTTPUnsupportedMediaType() 339 | func(req, resp) 340 | 341 | def handle_response_body(res, req, resp): 342 | resp.etag = res.generate_etag(req, resp) 343 | resp.last_modified = res.last_modified(req, resp) 344 | resp.expires = res.expires(req, resp) 345 | 346 | # Generate the body 347 | func = first_match(res.content_types_provided, req, resp, resp.content_type) 348 | if func is None: 349 | raise webmachine.exc.HTTPInternalServerError() 350 | 351 | body = func(req, resp) 352 | 353 | if not resp.content_type: 354 | resp.content_type = "text/plain" 355 | 356 | # Handle our content encoding. 357 | encoding = resp.content_encoding 358 | if encoding: 359 | func = first_match(res.encodings_provided, req, resp, encoding) 360 | if func is None: 361 | raise webmachine.exc.HTTPInternalServerError() 362 | resp.body = func(resp.body) 363 | resp['Content-Encoding'] = encoding 364 | 365 | if not isinstance(body, basestring) and hasattr(body, '__iter__'): 366 | resp._container = body 367 | resp._is_string = False 368 | else: 369 | resp._container = [body] 370 | resp._is_string = True 371 | 372 | 373 | TRANSITIONS = { 374 | b03: (200, c03), # Options? 375 | b04: (413, b03), # Request entity too large? 376 | b05: (415, b04), # Unknown Content-Type? 377 | b06: (501, b05), # Unknown or unsupported Content-* header? 378 | b07: (403, b06), # Forbidden? 379 | b08: (b07, 401), # Authorized? 380 | b09: (400, b08), # Malformed? 381 | b10: (b09, 405), # Is method allowed? 382 | b11: (414, b10), # URI too long? 383 | b12: (b11, 501), # Known method? 384 | b13: (b12, 503), # Service available? 385 | c03: (c04, d04), # Accept exists? 386 | c04: (d04, 406), # Acceptable media type available? 387 | d04: (d05, e05), # Accept-Language exists? 388 | d05: (e05, 406), # Accept-Language available? 389 | e05: (e06, f06), # Accept-Charset exists? 390 | e06: (f06, 406), # Acceptable charset available? 391 | f06: (f07, g07), # Accept-Encoding exists? 392 | f07: (g07, 406), # Acceptable encoding available? 393 | g07: (g08, h07), # Resource exists? 394 | g08: (g09, h10), # If-Match exists? 395 | g09: (h10, g11), # If-Match: * exists? 396 | g11: (h10, 412), # Etag in If-Match? 397 | h07: (412, i07), # If-Match: * exists? 398 | h10: (h11, i12), # If-Unmodified-Since exists? 399 | h11: (h12, i12), # If-Unmodified-Since is valid date? 400 | h12: (412, i12), # Last-Modified > If-Unmodified-Since? 401 | i04: (301, p03), # Apply to a different URI? 402 | i07: (i04, k07), # PUT? 403 | i12: (i13, l13), # If-None-Match exists? 404 | i13: (j18, k13), # If-None-Match: * exists? 405 | j18: (304, 412), # GET/HEAD? 406 | k05: (301, l05), # Resource moved permanently? 407 | k07: (k05, l07), # Resource previously existed? 408 | k13: (j18, l13), # Etag in If-None-Match? 409 | l05: (307, m05), # Resource moved temporarily? 410 | l07: (m07, 404), # POST? 411 | l13: (l14, m16), # If-Modified-Since exists? 412 | l14: (l15, m16), # If-Modified-Since is valid date? 413 | l15: (m16, l17), # If-Modified-Since > Now? 414 | l17: (m16, 304), # Last-Modified > If-Modified-Since? 415 | m05: (n05, 410), # POST? 416 | m07: (n11, 404), # Server permits POST to missing resource? 417 | m16: (m20, n16), # DELETE? 418 | m20: (m20b, 500), # DELETE enacted immediately? 419 | m20b: (o20, 202), # Delete completeed? 420 | m20: (o20, 202), # Delete enacted? 421 | n05: (n11, 410), # Server permits POST to missing resource? 422 | n11: (303, p11), # Redirect? 423 | n16: (n11, o16), # POST? 424 | o14: (409, p11), # Conflict? 425 | o16: (o14, o18), # PUT? 426 | o18: (300, 200), # Multiple representations? 427 | o20: (o18, 204), # Response includes entity? 428 | p03: (409, p11), # Conflict? 429 | p11: (201, o20) # New resource? 430 | } 431 | -------------------------------------------------------------------------------- /webmachine/exc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT. 4 | # See the NOTICE for more information. 5 | 6 | 7 | from django.http import HttpResponse 8 | from django import template 9 | 10 | 11 | class HTTPException(Exception): 12 | 13 | def __init__(self, message, resp): 14 | Exception.__init__(self, message) 15 | self.__dict__['response'] = resp 16 | 17 | def __call__(self): 18 | return self.response 19 | 20 | class DjangoHttpException(HttpResponse, HTTPException): 21 | status_code = 200 22 | title = None 23 | explanation = '' 24 | body_template = """\ 25 | {{explanation|safe}}

26 | {{detail|safe}} 27 | {{comment|safe}}""" 28 | 29 | ## Set this to True for responses that should have no request body 30 | empty_body = False 31 | 32 | def __init__(self, detail=None, body_template=None, comment=None, **kw): 33 | HttpResponse.__init__( 34 | self, 35 | status = '%s %s' % (self.code, self.title), 36 | **kw) 37 | Exception.__init__(self, detail) 38 | 39 | 40 | if comment is not None: 41 | self.comment = comment 42 | 43 | if body_template is not None: 44 | self.body_template = body_template 45 | if isinstance(self.explanation, (list, tuple)): 46 | self.explanation = "

%s

" % "
".join(self.explanation) 47 | 48 | 49 | if not self.empty_body: 50 | t = template.Template(self.body_template) 51 | c = template.Context(dict( 52 | detail=detail, 53 | explanation=self.explanation, 54 | comment=comment)) 55 | 56 | self._container = [t.render(c)] 57 | else: 58 | self._container = [''] 59 | self._is_string = True 60 | 61 | 62 | 63 | 64 | class HTTPOk(DjangoHttpException): 65 | """ return response with Status 200 """ 66 | title = "OK" 67 | 68 | class HTTPError(DjangoHttpException): 69 | code = 400 70 | 71 | ############################################################ 72 | ## 2xx success 73 | ############################################################ 74 | 75 | class HTTPCreated(HTTPOk): 76 | code = 201 77 | title = 'Created' 78 | 79 | class HTTPAccepted(HTTPOk): 80 | code = 202 81 | title = 'Accepted' 82 | explanation = 'The request is accepted for processing.' 83 | 84 | class HTTPNonAuthoritativeInformation(HTTPOk): 85 | code = 203 86 | title = 'Non-Authoritative Information' 87 | 88 | class HTTPNoContent(HTTPOk): 89 | code = 204 90 | title = 'No Content' 91 | empty_body = True 92 | 93 | class HTTPResetContent(HTTPOk): 94 | code = 205 95 | title = 'Reset Content' 96 | empty_body = True 97 | 98 | class HTTPPartialContent(HTTPOk): 99 | code = 206 100 | title = 'Partial Content' 101 | 102 | ############################################################ 103 | ## 4xx client error 104 | ############################################################ 105 | 106 | class HTTPClientError(HTTPError): 107 | """ 108 | base class for the 400's, where the client is in error 109 | 110 | This is an error condition in which the client is presumed to be 111 | in-error. This is an expected problem, and thus is not considered 112 | a bug. A server-side traceback is not warranted. Unless specialized, 113 | this is a '400 Bad Request' 114 | """ 115 | code = 400 116 | title = 'Bad Request' 117 | explanation = ('The server could not comply with the request since\r\n' 118 | 'it is either malformed or otherwise incorrect.\r\n') 119 | 120 | class HTTPBadRequest(HTTPClientError): 121 | pass 122 | 123 | class HTTPUnauthorized(HTTPClientError): 124 | code = 401 125 | title = 'Unauthorized' 126 | explanation = ( 127 | 'This server could not verify that you are authorized to\r\n' 128 | 'access the document you requested. Either you supplied the\r\n' 129 | 'wrong credentials (e.g., bad password), or your browser\r\n' 130 | 'does not understand how to supply the credentials required.\r\n') 131 | 132 | class HTTPPaymentRequired(HTTPClientError): 133 | code = 402 134 | title = 'Payment Required' 135 | explanation = ('Access was denied for financial reasons.') 136 | 137 | class HTTPForbidden(HTTPClientError): 138 | code = 403 139 | title = 'Forbidden' 140 | explanation = ('Access was denied to this resource.') 141 | 142 | class HTTPNotFound(HTTPClientError): 143 | code = 404 144 | title = 'Not Found' 145 | explanation = ('The resource could not be found.') 146 | 147 | class HTTPMethodNotAllowed(HTTPClientError): 148 | code = 405 149 | title = 'Method Not Allowed' 150 | 151 | class HTTPNotAcceptable(HTTPClientError): 152 | code = 406 153 | title = 'Not Acceptable' 154 | 155 | 156 | class HTTPProxyAuthenticationRequired(HTTPClientError): 157 | code = 407 158 | title = 'Proxy Authentication Required' 159 | explanation = ('Authentication with a local proxy is needed.') 160 | 161 | class HTTPRequestTimeout(HTTPClientError): 162 | code = 408 163 | title = 'Request Timeout' 164 | explanation = ('The server has waited too long for the request to ' 165 | 'be sent by the client.') 166 | 167 | class HTTPConflict(HTTPClientError): 168 | code = 409 169 | title = 'Conflict' 170 | explanation = ('There was a conflict when trying to complete ' 171 | 'your request.') 172 | 173 | class HTTPGone(HTTPClientError): 174 | code = 410 175 | title = 'Gone' 176 | explanation = ('This resource is no longer available. No forwarding ' 177 | 'address is given.') 178 | 179 | class HTTPLengthRequired(HTTPClientError): 180 | code = 411 181 | title = 'Length Required' 182 | explanation = ('Content-Length header required.') 183 | 184 | class HTTPPreconditionFailed(HTTPClientError): 185 | code = 412 186 | title = 'Precondition Failed' 187 | explanation = ('Request precondition failed.') 188 | 189 | class HTTPRequestEntityTooLarge(HTTPClientError): 190 | code = 413 191 | title = 'Request Entity Too Large' 192 | explanation = ('The body of your request was too large for this server.') 193 | 194 | class HTTPRequestURITooLong(HTTPClientError): 195 | code = 414 196 | title = 'Request-URI Too Long' 197 | explanation = ('The request URI was too long for this server.') 198 | 199 | class HTTPUnsupportedMediaType(HTTPClientError): 200 | code = 415 201 | title = 'Unsupported Media Type' 202 | 203 | 204 | class HTTPRequestRangeNotSatisfiable(HTTPClientError): 205 | code = 416 206 | title = 'Request Range Not Satisfiable' 207 | explanation = ('The Range requested is not available.') 208 | 209 | class HTTPExpectationFailed(HTTPClientError): 210 | code = 417 211 | title = 'Expectation Failed' 212 | explanation = ('Expectation failed.') 213 | 214 | class HTTPUnprocessableEntity(HTTPClientError): 215 | ## Note: from WebDAV 216 | code = 422 217 | title = 'Unprocessable Entity' 218 | explanation = 'Unable to process the contained instructions' 219 | 220 | class HTTPLocked(HTTPClientError): 221 | ## Note: from WebDAV 222 | code = 423 223 | title = 'Locked' 224 | explanation = ('The resource is locked') 225 | 226 | class HTTPFailedDependency(HTTPClientError): 227 | ## Note: from WebDAV 228 | code = 424 229 | title = 'Failed Dependency' 230 | explanation = ('The method could not be performed because the requested ' 231 | 'action dependended on another action and that action failed') 232 | 233 | ############################################################ 234 | ## 5xx Server Error 235 | ############################################################ 236 | 237 | 238 | class HTTPServerError(HTTPError): 239 | """ 240 | base class for the 500's, where the server is in-error 241 | 242 | This is an error condition in which the server is presumed to be 243 | in-error. This is usually unexpected, and thus requires a traceback; 244 | ideally, opening a support ticket for the customer. Unless specialized, 245 | this is a '500 Internal Server Error' 246 | """ 247 | code = 500 248 | title = 'Internal Server Error' 249 | explanation = ( 250 | 'The server has either erred or is incapable of performing\r\n' 251 | 'the requested operation.\r\n') 252 | 253 | class HTTPInternalServerError(HTTPServerError): 254 | pass 255 | 256 | class HTTPNotImplemented(HTTPServerError): 257 | code = 501 258 | title = 'Not Implemented' 259 | 260 | 261 | class HTTPBadGateway(HTTPServerError): 262 | code = 502 263 | title = 'Bad Gateway' 264 | explanation = ('Bad gateway.') 265 | 266 | class HTTPServiceUnavailable(HTTPServerError): 267 | code = 503 268 | title = 'Service Unavailable' 269 | explanation = ('The server is currently unavailable. ' 270 | 'Please try again at a later time.') 271 | 272 | class HTTPGatewayTimeout(HTTPServerError): 273 | code = 504 274 | title = 'Gateway Timeout' 275 | explanation = ('The gateway has timed out.') 276 | 277 | class HTTPVersionNotSupported(HTTPServerError): 278 | code = 505 279 | title = 'HTTP Version Not Supported' 280 | explanation = ('The HTTP version is not supported.') 281 | 282 | class HTTPInsufficientStorage(HTTPServerError): 283 | code = 507 284 | title = 'Insufficient Storage' 285 | explanation = ('There was not enough space to save the resource') 286 | 287 | -------------------------------------------------------------------------------- /webmachine/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import base64 7 | import hmac 8 | try: 9 | import hashlib 10 | _sha = hashlib.sha1 11 | except ImportError: 12 | import sha 13 | _sha = sha 14 | 15 | from django.conf import settings 16 | from django import forms 17 | 18 | 19 | class OAuthAuthenticationForm(forms.Form): 20 | oauth_token = forms.CharField(widget=forms.HiddenInput) 21 | oauth_callback = forms.CharField(widget=forms.HiddenInput, required=False) 22 | authorize_access = forms.BooleanField(required=True) 23 | csrf_signature = forms.CharField(widget=forms.HiddenInput) 24 | 25 | def __init__(self, *args, **kwargs): 26 | forms.Form.__init__(self, *args, **kwargs) 27 | 28 | self.fields['csrf_signature'].initial = self.initial_csrf_signature 29 | 30 | def clean_csrf_signature(self): 31 | sig = self.cleaned_data['csrf_signature'] 32 | token = self.cleaned_data['oauth_token'] 33 | 34 | sig1 = OAuthAuthenticationForm.get_csrf_signature(settings.SECRET_KEY, 35 | token) 36 | if sig != sig1: 37 | raise forms.ValidationError("CSRF signature is not valid") 38 | 39 | return sig 40 | 41 | def initial_csrf_signature(self): 42 | token = self.initial['oauth_token'] 43 | return OAuthAuthenticationForm.get_csrf_signature( 44 | settings.SECRET_KEY, token) 45 | 46 | @staticmethod 47 | def get_csrf_signature(key, token): 48 | """ Check signature """ 49 | hashed = hmac.new(key, token, _sha) 50 | return base64.b64encode(hashed.digest()) 51 | 52 | -------------------------------------------------------------------------------- /webmachine/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benoitc/dj-webmachine/77653d73de57388b712eaf50de8c32ec70c182fa/webmachine/helpers/__init__.py -------------------------------------------------------------------------------- /webmachine/helpers/serialize.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import decimal 7 | import datetime 8 | import re 9 | import time 10 | 11 | from django.db.models import Model 12 | from django.db.models.query import QuerySet 13 | from django.utils.encoding import smart_unicode 14 | 15 | 16 | re_date = re.compile('^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$') 17 | re_time = re.compile('^([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3})?$') 18 | re_datetime = re.compile('^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])(\D?([01]\d|2[0-3])\D?([0-5]\d)\D?([0-5]\d)?\D?(\d{3})?([zZ]|([\+-])([01]\d|2[0-3])\D?([0-5]\d)?)?)?$') 19 | re_decimal = re.compile('^(\d+)\.(\d+)$') 20 | 21 | 22 | __all__ = ['Serializer', 'JSONSerializer', 'value_to_emittable', 23 | 'value_to_python'] 24 | 25 | try: 26 | import json 27 | except ImportError: 28 | import django.utils.simplejson as json 29 | 30 | try: 31 | import cStringIO as StringIO 32 | except ImportError: 33 | import StringIO 34 | 35 | 36 | class Serializer(object): 37 | 38 | def __init__(self, fields=None, exclude=None): 39 | self.fields = fields 40 | self.exclude = exclude 41 | 42 | def _to_string(self, value): 43 | return value 44 | 45 | def _to_python(self, value): 46 | return value 47 | 48 | def serialize(self, value): 49 | value = value_to_emittable(value, fields=self.fields, 50 | exclude=self.exclude) 51 | return self._to_string(value) 52 | 53 | def unserialize(self, value): 54 | if isinstance(value, basestring): 55 | value = StringIO.StringIO(value) 56 | 57 | return value_to_python(self._to_python(value)) 58 | 59 | class JSONSerializer(Serializer): 60 | 61 | def _to_string(self, value): 62 | stream = StringIO.StringIO() 63 | json.dump(value, stream) 64 | return stream.getvalue() 65 | 66 | def _to_python(self, value): 67 | return json.load(value) 68 | 69 | 70 | 71 | def dict_to_emittable(value, fields=None, exclude=None): 72 | """ convert a dict to json """ 73 | return dict([(k, value_to_emittable(v, fields=fields, 74 | exclude=exclude)) for k, v in value.iteritems()]) 75 | 76 | def list_to_emittable(value, fields=None, exclude=None): 77 | """ convert a list to json """ 78 | return [value_to_emittable(item, fields=fields, exclude=exclude) for item in value] 79 | 80 | def relm_to_emittable(value): 81 | return value_to_emittable(value.all()) 82 | 83 | 84 | def fk_to_emittable(value, field, fields=None, exclude=None): 85 | return value_to_emittable(getattr(value, field.name)) 86 | 87 | 88 | def m2m_to_emittable(value, field, fields=None, exclude=None): 89 | return [model_to_emittable(m, fields=fields, exclude=exclude) \ 90 | for m in getattr(value, field.name).iterator() ] 91 | 92 | def qs_to_emittable(value, fields=None, exclude=None): 93 | return [value_to_emittable(v, fields=fields, exclude=exclude) for v in value] 94 | 95 | def model_to_emittable(instance, fields=None, exclude=None): 96 | meta = instance._meta 97 | if not fields and not exclude: 98 | ret = {} 99 | for f in meta.fields: 100 | ret[f.attname] = value_to_emittable(getattr(instance, 101 | f.attname)) 102 | 103 | fields = dir(instance.__class__) + ret.keys() 104 | extra = [k for k in dir(instance) if k not in fields] 105 | 106 | for k in extra: 107 | ret[k] = value_to_emittable(getattr(instance, k)) 108 | else: 109 | fields_list = [] 110 | fields_iter = iter(meta.local_fields + meta.virtual_fields + meta.many_to_many) 111 | # fields_iter = iter(meta.fields + meta.many_to_many) 112 | for f in fields_iter: 113 | value = None 114 | if fields is not None and not f.name in fields: 115 | continue 116 | if exclude is not None and f.name in exclude: 117 | continue 118 | 119 | if f in meta.many_to_many: 120 | if f.serialize: 121 | value = m2m_to_emittable(instance, f, fields=fields, 122 | exclude=exclude) 123 | else: 124 | if f.serialize: 125 | if not f.rel: 126 | value = value_to_emittable(getattr(instance, 127 | f.attname), fields=fields, exclude=exclude) 128 | else: 129 | value = fk_to_emittable(instance, f, 130 | fields=fields, exclude=exclude) 131 | 132 | if value is None: 133 | continue 134 | 135 | fields_list.append((f.name, value)) 136 | 137 | ret = dict(fields_list) 138 | return ret 139 | 140 | def value_to_emittable(value, fields=None, exclude=None): 141 | """ convert a value to json using appropriate regexp. 142 | For Dates we use ISO 8601. Decimal are converted to string. 143 | """ 144 | if isinstance(value, QuerySet): 145 | value = qs_to_emittable(value, fields=fields, exclude=exclude) 146 | elif isinstance(value, datetime.datetime): 147 | value = value.replace(microsecond=0).isoformat() + 'Z' 148 | elif isinstance(value, datetime.date): 149 | value = value.isoformat() 150 | elif isinstance(value, datetime.time): 151 | value = value.replace(microsecond=0).isoformat() 152 | elif isinstance(value, decimal.Decimal): 153 | value = str(value) 154 | elif isinstance(value, list): 155 | value = list_to_emittable(value, fields=fields, exclude=exclude) 156 | elif isinstance(value, dict): 157 | value = dict_to_emittable(value, fields=fields, 158 | exclude=exclude) 159 | elif isinstance(value, Model): 160 | value = model_to_emittable(value, fields=fields, 161 | exclude=exclude) 162 | 163 | elif repr(value).startswith("'; 311 | } 312 | return prev; 313 | }; 314 | 315 | function drawTrace() { 316 | drawDecision(trace[0]); 317 | for (var i = 1; i < trace.length; i++) { 318 | drawPath(trace[i].path); 319 | drawDecision(trace[i]); 320 | } 321 | 322 | drawPath(response.path); 323 | drawResponse(); 324 | }; 325 | 326 | function drawResponse() { 327 | if (response.type == 'normal') { 328 | var context = canvas.getContext('2d'); 329 | context.strokeStyle=HIGHLIGHT; 330 | context.lineWidth=4; 331 | 332 | context.beginPath(); 333 | context.rect(response.x-(response.width/2), 334 | response.y-19, 335 | response.width, 336 | 38); 337 | context.stroke(); 338 | } else { 339 | var context = canvas.getContext('2d'); 340 | context.strokeStyle='#ff0000'; 341 | context.lineWidth=4; 342 | 343 | context.beginPath(); 344 | context.arc(response.x, response.y, 19, 345 | 0, 2*3.14159, false); 346 | context.stroke(); 347 | 348 | } 349 | }; 350 | 351 | function drawDecision(dec) { 352 | var context = canvas.getContext('2d'); 353 | 354 | if (dec.previewCalls == '') 355 | context.strokeStyle=REGULAR; 356 | else 357 | context.strokeStyle=HIGHLIGHT; 358 | context.lineWidth=4; 359 | 360 | context.beginPath(); 361 | context.moveTo(dec.x, dec.y-19); 362 | context.lineTo(dec.x+19, dec.y); 363 | context.lineTo(dec.x, dec.y+19); 364 | context.lineTo(dec.x-19, dec.y); 365 | context.closePath(); 366 | context.stroke(); 367 | }; 368 | 369 | function drawPath(path) { 370 | var context = canvas.getContext('2d'); 371 | context.strokeStyle=REGULAR; 372 | context.lineWidth=4; 373 | 374 | context.beginPath(); 375 | context.moveTo(path[0].x1, path[0].y1); 376 | for (var p = 0; p < path.length; p++) { 377 | context.lineTo(path[p].x2, path[p].y2); 378 | } 379 | context.stroke(); 380 | }; 381 | 382 | function getSeg(p1, p2, last) { 383 | var seg = { 384 | x1:cols[p1[0]], 385 | y1:rows[p1.slice(1)] 386 | }; 387 | if (ends[p2]) { 388 | seg.x2 = cols[ends[p2].col]; 389 | seg.y2 = rows[ends[p2].row]; 390 | } else { 391 | seg.x2 = cols[p2[0]]; 392 | seg.y2 = rows[p2.slice(1)]; 393 | } 394 | 395 | if (seg.x1 == seg.x2) { 396 | if (seg.y1 < seg.y2) { 397 | seg.y1 = seg.y1+19; 398 | if (last) seg.y2 = seg.y2-19; 399 | } else { 400 | seg.y1 = seg.y1-19; 401 | if (last) seg.y2 = seg.y2+19; 402 | } 403 | } else { 404 | //assume seg.y1 == seg.y2 405 | if (seg.x1 < seg.x2) { 406 | seg.x1 = seg.x1+19; 407 | if (last) seg.x2 = seg.x2-(ends[p2] ? (ends[p2].width/2) : 19); 408 | } else { 409 | seg.x1 = seg.x1-19; 410 | if (last) seg.x2 = seg.x2+(ends[p2] ? (ends[p2].width/2) : 19); 411 | } 412 | } 413 | return seg; 414 | }; 415 | 416 | function traceDecision(name) { 417 | for (var i = trace.length-1; i >= 0; i--) 418 | if (trace[i].d == name) return trace[i]; 419 | }; 420 | 421 | var detailPanels = {}; 422 | function initDetailPanels() { 423 | var windowWidth = document.getElementById('sizetest').clientWidth; 424 | var infoPanel = document.getElementById('infopanel'); 425 | var panelWidth = windowWidth-infoPanel.offsetLeft; 426 | 427 | var panels = { 428 | 'request': document.getElementById('requestdetail'), 429 | 'response': document.getElementById('responsedetail'), 430 | 'decision': document.getElementById('decisiondetail') 431 | }; 432 | 433 | var tabs = { 434 | 'request': document.getElementById('requesttab'), 435 | 'response': document.getElementById('responsetab'), 436 | 'decision': document.getElementById('decisiontab') 437 | }; 438 | 439 | var decisionId = document.getElementById('decisionid'); 440 | var decisionCalls = document.getElementById('decisioncalls'); 441 | var callInput = document.getElementById('callinput'); 442 | var callOutput = document.getElementById('calloutput'); 443 | 444 | var lastUsedPanelWidth = windowWidth-infoPanel.offsetLeft; 445 | 446 | var setPanelWidth = function(width) { 447 | infoPanel.style.left = (windowWidth-width)+'px'; 448 | canvas.style.marginRight = (width+20)+'px'; 449 | panelWidth = width; 450 | }; 451 | setPanelWidth(panelWidth); 452 | 453 | var ensureVisible = function() { 454 | if (windowWidth-infoPanel.offsetLeft < 10) 455 | setPanelWidth(lastUsedPanelWidth); 456 | }; 457 | 458 | var decChoices = ''; 459 | for (var i = 0; i < trace.length; i++) { 460 | decChoices += ''; 461 | } 462 | decisionId.innerHTML = decChoices; 463 | decisionId.selectedIndex = -1; 464 | 465 | decisionId.onchange = function() { 466 | detailPanels.setDecision(traceDecision(decisionId.value)); 467 | } 468 | 469 | detailPanels.setDecision = function(dec) { 470 | decisionId.value = dec.d; 471 | 472 | var calls = []; 473 | for (var i = 0; i < dec.calls.length; i++) { 474 | calls.push(''); 477 | } 478 | decisionCalls.innerHTML = calls.join(''); 479 | decisionCalls.selectedIndex = 0; 480 | 481 | decisionCalls.onchange(); 482 | }; 483 | 484 | detailPanels.show = function(name) { 485 | for (p in panels) { 486 | if (p == name) { 487 | panels[p].style.display = 'block'; 488 | tabs[p].className = 'selectedtab'; 489 | } 490 | else { 491 | panels[p].style.display = 'none'; 492 | tabs[p].className = ''; 493 | } 494 | } 495 | ensureVisible(); 496 | }; 497 | 498 | detailPanels.hide = function() { 499 | setPanelWidth(0); 500 | } 501 | 502 | decisionCalls.onchange = function() { 503 | var val = decisionCalls.value; 504 | if (val) { 505 | var dec = traceDecision(val.substring(0, val.indexOf('-'))); 506 | var call = dec.calls[parseInt(val.substring(val.indexOf('-')+1, val.length))]; 507 | 508 | if (call.output != "wmtrace_not_exported") { 509 | callInput.style.color='#000000'; 510 | callInput.innerHTML = call.input; 511 | if (call.output != null) { 512 | callOutput.style.color = '#000000'; 513 | callOutput.innerHTML = call.output; 514 | } else { 515 | callOutput.style.color = '#ff0000'; 516 | callOutput.textContent = 'Error: '+call.module+':'+call['function']+' never returned'; 517 | } 518 | } else { 519 | callInput.style.color='#999999'; 520 | callInput.textContent = call.module+':'+call['function']+' was not exported'; 521 | callOutput.textContent = ''; 522 | } 523 | } else { 524 | callInput.textContent = ''; 525 | callOutput.textContent = ''; 526 | } 527 | }; 528 | 529 | var headersList = function(headers) { 530 | var h = ''; 531 | for (n in headers) h += '
  • '+n+': '+headers[n]; 532 | return h; 533 | }; 534 | 535 | document.getElementById('requestmethod').innerHTML = request.method; 536 | document.getElementById('requestpath').innerHTML = request.path; 537 | document.getElementById('requestheaders').innerHTML = headersList(request.headers); 538 | document.getElementById('requestbody').innerHTML = request.body; 539 | 540 | document.getElementById('responsecode').innerHTML = response.code; 541 | document.getElementById('responseheaders').innerHTML = headersList(response.headers); 542 | document.getElementById('responsebody').innerHTML = response.body; 543 | 544 | 545 | var infoControls = document.getElementById('infocontrols'); 546 | var md = false; 547 | var dragged = false; 548 | var msoff = 0; 549 | infoControls.onmousedown = function(ev) { 550 | md = true; 551 | dragged = false; 552 | msoff = ev.clientX-infoPanel.offsetLeft; 553 | }; 554 | 555 | infoControls.onclick = function(ev) { 556 | if (dragged) { 557 | lastUsedPanelWidth = panelWidth; 558 | } 559 | else if (panelWidth < 10) { 560 | switch(ev.target.id) { 561 | case 'requesttab': detailPanels.show('request'); break; 562 | case 'responsetab': detailPanels.show('response'); break; 563 | case 'decisiontab': detailPanels.show('decision'); break; 564 | default: ensureVisible(); 565 | } 566 | } else { 567 | var name = 'none'; 568 | switch(ev.target.id) { 569 | case 'requesttab': name = 'request'; break; 570 | case 'responsetab': name = 'response'; break; 571 | case 'decisiontab': name = 'decision'; break; 572 | } 573 | 574 | if (panels[name] && panels[name].style.display != 'block') 575 | detailPanels.show(name); 576 | else 577 | detailPanels.hide(); 578 | } 579 | 580 | return false; 581 | }; 582 | 583 | document.onmousemove = function(ev) { 584 | if (md) { 585 | dragged = true; 586 | panelWidth = windowWidth-(ev.clientX-msoff); 587 | if (panelWidth < 0) { 588 | panelWidth = 0; 589 | infoPanel.style.left = windowWidth+"px"; 590 | } 591 | else if (panelWidth > windowWidth-21) { 592 | panelWidth = windowWidth-21; 593 | infoPanel.style.left = '21px'; 594 | } 595 | else 596 | infoPanel.style.left = (ev.clientX-msoff)+"px"; 597 | 598 | canvas.style.marginRight = panelWidth+20+"px"; 599 | return false; 600 | } 601 | }; 602 | 603 | document.onmouseup = function() { md = false; }; 604 | 605 | window.onresize = function() { 606 | windowWidth = document.getElementById('sizetest').clientWidth; 607 | infoPanel.style.left = windowWidth-panelWidth+'px'; 608 | }; 609 | }; 610 | 611 | window.onload = function() { 612 | canvas = document.getElementById('v3map'); 613 | 614 | initDetailPanels(); 615 | 616 | var scale = 0.25; 617 | var coy = canvas.offsetTop; 618 | function findDecision(ev) { 619 | var x = (ev.clientX+window.pageXOffset)/scale; 620 | var y = (ev.clientY+window.pageYOffset-coy)/scale; 621 | 622 | for (var i = trace.length-1; i >= 0; i--) { 623 | if (x >= trace[i].x-19 && x <= trace[i].x+19 && 624 | y >= trace[i].y-19 && y <= trace[i].y+19) 625 | return trace[i]; 626 | } 627 | }; 628 | 629 | var preview = document.getElementById('preview'); 630 | var previewId = document.getElementById('previewid'); 631 | var previewCalls = document.getElementById('previewcalls'); 632 | function previewDecision(dec) { 633 | preview.style.left = (dec.x*scale)+'px'; 634 | preview.style.top = (dec.y*scale+coy+15)+'px'; 635 | preview.style.display = 'block'; 636 | previewId.textContent = dec.d; 637 | 638 | previewCalls.innerHTML = dec.previewCalls; 639 | }; 640 | 641 | function overResponse(ev) { 642 | var x = (ev.clientX+window.pageXOffset)/scale; 643 | var y = (ev.clientY+window.pageYOffset-coy)/scale; 644 | 645 | return (x >= response.x-(response.width/2) 646 | && x <= response.x+(response.width/2) 647 | && y >= response.y-19 && y <= response.y+19); 648 | }; 649 | 650 | decorateTrace(); 651 | 652 | var bg = new Image(3138, 2184); 653 | 654 | function drawMap() { 655 | var ctx = canvas.getContext("2d"); 656 | 657 | ctx.save(); 658 | ctx.scale(1/scale, 1/scale); 659 | ctx.fillStyle = '#ffffff'; 660 | ctx.fillRect(0, 0, 3138, 2184); 661 | ctx.restore(); 662 | 663 | ctx.drawImage(bg, 0, 0); 664 | drawTrace(); 665 | }; 666 | 667 | bg.onload = function() { 668 | canvas.getContext("2d").scale(scale, scale); 669 | drawMap(scale); 670 | 671 | canvas.onmousemove = function(ev) { 672 | if (findDecision(ev)) { 673 | canvas.style.cursor = 'pointer'; 674 | previewDecision(findDecision(ev)); 675 | } 676 | else { 677 | preview.style.display = 'none'; 678 | if (overResponse(ev)) 679 | canvas.style.cursor = 'pointer'; 680 | else 681 | canvas.style.cursor = 'default'; 682 | } 683 | }; 684 | 685 | canvas.onclick = function(ev) { 686 | var dec = findDecision(ev); 687 | if (dec) { 688 | detailPanels.setDecision(dec); 689 | detailPanels.show('decision'); 690 | } else if (overResponse(ev)) { 691 | detailPanels.show('response'); 692 | } 693 | }; 694 | 695 | document.getElementById('zoomin').onclick = function() { 696 | scale = scale*2; 697 | canvas.getContext("2d").scale(2, 2); 698 | drawMap(); 699 | }; 700 | 701 | document.getElementById('zoomout').onclick = function() { 702 | scale = scale/2; 703 | canvas.getContext("2d").scale(0.5, 0.5); 704 | drawMap(); 705 | }; 706 | }; 707 | 708 | bg.onerror = function() { 709 | alert('Failed to load background image.'); 710 | }; 711 | 712 | bg.src = 'static/map.png'; 713 | }; 714 | -------------------------------------------------------------------------------- /webmachine/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import time 7 | import urllib 8 | import urlparse 9 | 10 | from django.contrib.auth.models import User 11 | from django.db import models 12 | 13 | from webmachine.util.const import KEY_SIZE, SECRET_SIZE, VERIFIER_SIZE, \ 14 | TOKEN_TYPES, PENDING, CONSUMER_STATES 15 | 16 | from webmachine.managers import ConsumerManager, TokenManager 17 | 18 | def generate_random(length=SECRET_SIZE): 19 | return User.objects.make_random_password(length=length) 20 | 21 | class Nonce(models.Model): 22 | token_key = models.CharField(max_length=KEY_SIZE) 23 | consumer_key = models.CharField(max_length=KEY_SIZE) 24 | key = models.CharField(max_length=255) 25 | 26 | class Consumer(models.Model): 27 | name = models.CharField(max_length=255) 28 | key = models.CharField(max_length=KEY_SIZE) 29 | secret = models.CharField(max_length=SECRET_SIZE) 30 | description = models.TextField() 31 | user = models.ForeignKey(User, null=True, blank=True, 32 | related_name="consumers_user") 33 | status = models.SmallIntegerField(choices=CONSUMER_STATES, 34 | default=PENDING) 35 | 36 | objects = ConsumerManager() 37 | 38 | def __str__(self): 39 | data = {'oauth_consumer_key': self.key, 40 | 'oauth_consumer_secret': self.secret} 41 | 42 | return urllib.urlencode(data) 43 | 44 | class Token(models.Model): 45 | key = models.CharField(max_length=KEY_SIZE) 46 | secret = models.CharField(max_length=SECRET_SIZE) 47 | token_type = models.SmallIntegerField(choices=TOKEN_TYPES) 48 | callback = models.CharField(max_length=2048) #URL 49 | callback_confirmed = models.BooleanField(default=False) 50 | verifier = models.CharField(max_length=VERIFIER_SIZE) 51 | consumer = models.ForeignKey(Consumer, 52 | related_name="tokens_consumer") 53 | timestamp = models.IntegerField(default=time.time()) 54 | user = models.ForeignKey(User, null=True, blank=True, 55 | related_name="tokens_user") 56 | is_approved = models.BooleanField(default=False) 57 | 58 | objects = TokenManager() 59 | 60 | def set_callback(self, callback): 61 | self.callback = callback 62 | self.callback_confirmed = True 63 | self.save() 64 | 65 | def set_verifier(self, verifier=None): 66 | if verifier is not None: 67 | self.verifier = verifier 68 | else: 69 | self.verifier = generate_random(VERIFIER_SIZE) 70 | self.save() 71 | 72 | def get_callback_url(self): 73 | if self.callback and self.verifier: 74 | # Append the oauth_verifier. 75 | parts = urlparse.urlparse(self.callback) 76 | scheme, netloc, path, params, query, fragment = parts[:6] 77 | if query: 78 | query = '%s&oauth_verifier=%s' % (query, self.verifier) 79 | else: 80 | query = 'oauth_verifier=%s' % self.verifier 81 | return urlparse.urlunparse((scheme, netloc, path, params, 82 | query, fragment)) 83 | return self.callback 84 | 85 | 86 | def to_string(self, only_key=False): 87 | token_dict = { 88 | 'oauth_token': self.key, 89 | 'oauth_token_secret': self.secret, 90 | 'oauth_callback_confirmed': self.callback_confirmed and 'true' or 'error', 91 | } 92 | 93 | if self.verifier: 94 | token_dict.update({ 'oauth_verifier': self.verifier }) 95 | 96 | if only_key: 97 | del token_dict['oauth_token_secret'] 98 | del token_dict['oauth_callback_confirmed'] 99 | 100 | return urllib.urlencode(token_dict) 101 | -------------------------------------------------------------------------------- /webmachine/route.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | """ 7 | Minimal Route handling 8 | ++++++++++++++++++++++ 9 | 10 | Combinating the power of Django and the :ref:`resources ` it's relatively easy to buid an api. The process is also eased using the WM object. dj-webmachine offer a way to create automatically resources by using the ``route`` decorator. 11 | 12 | Using this decorator, our helloworld example can be rewritten like that: 13 | 14 | .. code-block:: python 15 | 16 | 17 | from webmachine import wm 18 | 19 | import json 20 | @wm.route(r"^$") 21 | def hello(req, resp): 22 | return "

    hello world!

    " 23 | 24 | 25 | @wm.route(r"^$", provided=[("application/json", json.dumps)]) 26 | def hello_json(req, resp): 27 | return {"ok": True, "message": "hellow world"} 28 | 29 | and the urls.py: 30 | 31 | .. code-block:: python 32 | 33 | from django.conf.urls.defaults import * 34 | 35 | import webmachine 36 | 37 | webmachine.autodiscover() 38 | 39 | urlpatterns = patterns('', 40 | (r'^', include(webmachine.wm.urls)) 41 | ) 42 | 43 | The autodiscover will detect all resources modules and add then to the 44 | url dispatching. The route decorator works a little like the one in 45 | bottle_ or for that matter flask_ (though bottle was the first). 46 | 47 | This decorator works differently though. It creates full 48 | :class:`webmachine.resource.Resource` instancse registered in the wm 49 | object. So we are abble to provide all the features available in a 50 | resource: 51 | 52 | * settings which content is accepted, provided 53 | * assiciate serializers to the content types 54 | * throttling 55 | * authorization 56 | 57 | """ 58 | import webmachine.exc 59 | from webmachine.resource import Resource, RESOURCE_METHODS 60 | 61 | try: 62 | from cStringIO import StringIO 63 | except ImportError: 64 | from StringIO import StringIO 65 | 66 | def validate_ctype(value): 67 | if isinstance(value, basestring): 68 | return [value] 69 | elif not isinstance(value, list) and value is not None: 70 | raise TypeError("'%s' should be a list or a string, got %s" % 71 | (value, type(value))) 72 | return value 73 | 74 | 75 | def serializer_cb(serializer, method): 76 | if hasattr(serializer, method): 77 | return getattr(serializer, method) 78 | return serializer 79 | 80 | def build_ctypes(ctypes,method): 81 | for ctype in ctypes: 82 | if isinstance(ctype, tuple): 83 | cb = serializer_cb(ctype[1], method) 84 | yield ctype[0], cb 85 | else: 86 | yield ctype, lambda v: v 87 | 88 | 89 | class RouteResource(Resource): 90 | 91 | def __init__(self, pattern, fun, **kwargs): 92 | self.set_pattern(pattern, **kwargs) 93 | 94 | methods = kwargs.get('methods') or ['GET', 'HEAD'] 95 | if isinstance(methods, basestring): 96 | methods = [methods] 97 | 98 | elif not isinstance(methods, (list, tuple,)): 99 | raise TypeError("methods should be list or a tuple, '%s' provided" % type(methods)) 100 | 101 | # associate methods to the function 102 | self.methods = {} 103 | for m in methods: 104 | self.methods[m.upper()] = fun 105 | 106 | # build content provided list 107 | provided = validate_ctype(kwargs.get('provided') or \ 108 | ['text/html']) 109 | self.provided = list(build_ctypes(provided, "serialize")) 110 | 111 | # build content accepted list 112 | accepted = validate_ctype(kwargs.get('accepted')) or [] 113 | self.accepted = list(build_ctypes(accepted, "unserialize")) 114 | self.kwargs = kwargs 115 | 116 | # override method if needed 117 | for k, v in self.kwargs.items(): 118 | if k in RESOURCE_METHODS: 119 | setattr(self, k, self.wrap(v)) 120 | 121 | def set_pattern(self, pattern, **kwargs): 122 | self.url = (pattern, kwargs.get('name')) 123 | 124 | def update(self, fun, **kwargs): 125 | methods = kwargs.get('methods') or ['GET', 'HEAD'] 126 | if isinstance(methods, basestring): 127 | methods = [methods] 128 | elif not isinstance(methods, (list, tuple,)): 129 | raise TypeError("methods should be list or a tuple, '%s' provided" % type(methods)) 130 | 131 | # associate methods to the function 132 | for m in methods: 133 | self.methods[m.upper()] = fun 134 | 135 | # we probably should merge here 136 | provided = validate_ctype(kwargs.get('provided')) 137 | if provided is not None: 138 | provided = list(build_ctypes(provided, "serialize")) 139 | self.provided.extend(provided) 140 | 141 | accepted = validate_ctype(kwargs.get('accepted')) 142 | if accepted is not None: 143 | accepted = list(build_ctypes(accepted, "unserialize")) 144 | self.accepted.extend(accepted) 145 | 146 | 147 | def wrap(self, f, cb=None): 148 | def _wrapped(req, resp): 149 | if cb is not None: 150 | return cb(f(req, resp)) 151 | return f(req, resp) 152 | return _wrapped 153 | 154 | def first_match(self, media, expect): 155 | for key, value in media: 156 | if key == expect: 157 | return value 158 | return None 159 | 160 | def accept_body(self, req, resp): 161 | ctype = req.content_type or "application/octet-stream" 162 | mtype = ctype.split(";", 1)[0] 163 | funload = self.first_match(self.accepted, mtype) 164 | if funload is None: 165 | raise webmachine.exc.HTTPUnsupportedMediaType() 166 | req._raw_post_data = funload(req.raw_post_data) 167 | if isinstance(req._raw_post_data, basestring): 168 | req._stream = StringIO(req._raw_post_data) 169 | 170 | fun = self.methods[req.method] 171 | body = fun(req, resp) 172 | if isinstance(body, tuple): 173 | resp._container, resp.location = body 174 | else: 175 | resp._container = body 176 | 177 | return self.return_body(req, resp) 178 | 179 | def return_body(self, req, resp): 180 | fundump = self.first_match(self.provided, resp.content_type) 181 | if fundump is None: 182 | raise webmachine.exc.HTTPInternalServerError() 183 | resp._container = fundump(resp._container) 184 | if not isinstance(resp._container, basestring): 185 | resp._is_tring = False 186 | else: 187 | resp._container = [resp._container] 188 | resp._is_string = True 189 | return resp._container 190 | 191 | 192 | #### resources methods 193 | 194 | def allowed_methods(self, req, resp): 195 | return self.methods.keys() 196 | 197 | def format_suffix_accepted(self, req, resp): 198 | if 'formats' in self.kwargs: 199 | return self.kwargs['formats'] 200 | return [] 201 | 202 | def content_types_accepted(self, req, resp): 203 | if not self.accepted: 204 | return None 205 | return [(c, self.accept_body) for c, f in self.accepted] 206 | 207 | def content_types_provided(self, req, resp): 208 | fun = self.methods[req.method] 209 | if not self.provided: 210 | return [("text/html", self.wrap(fun))] 211 | 212 | return [(c, self.wrap(fun, f)) for c, f in self.provided] 213 | 214 | def delete_resource(self, req, resp): 215 | fun = self.methods['DELETE'] 216 | ret = fun(req, resp) 217 | if isinstance(ret, basestring) or hasattr(ret, '__iter__'): 218 | resp._container = ret 219 | self.return_body(req, resp) 220 | return True 221 | return False 222 | 223 | def post_is_create(self, req, resp): 224 | if req.method == 'POST': 225 | return True 226 | return False 227 | 228 | def created_location(self, req, resp): 229 | return resp.location 230 | 231 | def process_post(self, req, resp): 232 | return self.accept_body(req, resp) 233 | 234 | def multiple_choices(self, req, resp): 235 | return False 236 | 237 | def get_urls(self): 238 | from django.conf.urls.defaults import patterns, url 239 | url_kwargs = self.kwargs.get('url_kwargs') or {} 240 | 241 | if len(self.url) >2: 242 | url1 =url(self.url[0], self, name=self.url[1], kwargs=url_kwargs) 243 | else: 244 | url1 =url(self.url[0], self, kwargs=url_kwargs) 245 | 246 | return patterns('', url1) 247 | 248 | 249 | class WM(object): 250 | 251 | def __init__(self, name="webmachine", version=None): 252 | self.name = name 253 | self.version = version 254 | self.resources = {} 255 | self.routes = [] 256 | 257 | def route(self, pattern, **kwargs): 258 | """ 259 | A decorator that is used to register a new resource using 260 | this function to return response. 261 | 262 | **Parameters** 263 | 264 | :attr pattern: regular expression, like the one you give in 265 | your urls.py 266 | 267 | :attr methods: methods accepted on this function 268 | 269 | :attr provides: list of provided contents tpes and associated 270 | serializers:: 271 | 272 | [(MediaType, Handler)] 273 | 274 | 275 | :attr accepted: list of content you accept in POST/PUT with 276 | associated deserializers:: 277 | 278 | [(MediaType, Handler)] 279 | 280 | 281 | A serializer can be a simple callable taking a value or a class: 282 | 283 | .. code-block:: python 284 | 285 | class Myserializer(object): 286 | 287 | def unserialize(self, value): 288 | # ... do something to value 289 | return value 290 | 291 | def serialize(self, value): 292 | # ... do something to value 293 | return value 294 | 295 | 296 | :attr formats: return a list of format with their associated 297 | contenttype:: 298 | 299 | [(Suffix, MediaType)] 300 | 301 | :attr kwargs: any named parameter coresponding to a 302 | :ref:`resource method `. Each value is a callable 303 | taking a request and a response as arguments: 304 | 305 | .. code-block:: python 306 | 307 | def f(req, resp): 308 | pass 309 | 310 | """ 311 | def _decorated(func): 312 | self.add_route(pattern, func, **kwargs) 313 | return func 314 | return _decorated 315 | 316 | def _wrap_urls(self, f, pattern): 317 | from django.conf.urls.defaults import patterns, url, include 318 | def _wrapped(*args): 319 | return patterns('', 320 | url(pattern, include(f(*args))) 321 | ) 322 | return _wrapped 323 | 324 | def add_resource(self, klass, pattern=None): 325 | """ 326 | Add one :ref:`Resource class` to the routing. 327 | 328 | :attr klass: class inheriting from :class:webmachine.Resource 329 | :attr pattern: regexp. 330 | 331 | """ 332 | res = klass() 333 | if not pattern: 334 | if hasattr(res._meta, "resource_path"): 335 | kname = res._meta.resource_path 336 | else: 337 | kname = klass.__name__.lower() 338 | 339 | pattern = r'^%s/' % res._meta.app_label 340 | if kname: 341 | pattern = r'%s/' % kname 342 | res.get_urls = self._wrap_urls(res.get_urls, pattern) 343 | self.resources[pattern] = res 344 | 345 | def add_resources(self, *klasses): 346 | """ 347 | Allows you to add multiple Resource classes to the WM instance. You 348 | can also pass a pattern by using a tupple instead of simply 349 | provided the Resource class. Example:: 350 | 351 | (MyResource, r"^some/path$") 352 | 353 | """ 354 | for klass in klasses: 355 | if isinstance(klass, tuple): 356 | klass, pattern = klass 357 | else: 358 | pattern = None 359 | self.add_resource(klass, pattern=pattern) 360 | 361 | def add_route(self, pattern, func, **kwargs): 362 | if pattern in self.resources: 363 | res = self.resources[pattern] 364 | res.update(func, **kwargs) 365 | else: 366 | res = RouteResource(pattern, func, **kwargs) 367 | self.resources[pattern] = res 368 | 369 | self.routes.append((pattern, func, kwargs)) 370 | # associate the resource to the function 371 | setattr(func, "_wmresource", res) 372 | 373 | 374 | def get_urls(self): 375 | from django.conf.urls.defaults import patterns 376 | urlpatterns = patterns('') 377 | for pattern, resource in self.resources.items(): 378 | urlpatterns += resource.get_urls() 379 | return urlpatterns 380 | 381 | urls = property(get_urls) 382 | 383 | wm = WM() 384 | -------------------------------------------------------------------------------- /webmachine/templates/webmachine/authorize_token.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Authorize Token 6 | 7 | 8 |

    Authorize Token

    9 | 10 |
    11 | {{ form.as_table }} 12 |
    13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /webmachine/templates/wm/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | dj-webmachine - {%block title %}{% endblock %} 6 | {% block head %}{% endblock %} 7 | 8 | 9 | {% block content %}{% endblock %} 10 | {% block jscripts %}{% endblock %} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /webmachine/templates/wm/wmtrace.html: -------------------------------------------------------------------------------- 1 | {% extends "wm/base.html" %} 2 | 3 | {% block title %}Trace {{ fname }}{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 8 | 9 | 15 | {% endblock head %} 16 | {% block content %} 17 | 18 |
    19 | 20 | 21 |
    22 | 23 |
    24 |
    25 |
    26 |
      27 |
      28 |
      29 |
      30 |
      31 |
      Q
      32 |
      R
      33 |
      D
      34 |
      35 |
      36 |
      37 | 39 |
      40 |
        41 |
        42 |
        43 |
        44 |
            45 | 46 |
        47 |
        48 |
        49 | Decisions: 50 |
        51 |
        52 | Calls: 53 |
        54 |
        55 | Input:
        
        56 |         
        57 |
        58 | Output:
        
        59 |         
        60 |
        61 |
        62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /webmachine/templates/wm/wmtrace_list.html: -------------------------------------------------------------------------------- 1 | {% extends "wm/base.html" %} 2 | 3 | {% block title %}Trace List for {{ path }}{% endblock %} 4 | 5 | {% block content %} 6 |

        Traces in {{path}}

        7 | 8 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /webmachine/throttle.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import time 7 | 8 | from django.core.cache import cache 9 | 10 | class Limiter(object): 11 | 12 | def __init__(self, res, **options): 13 | self.res = res 14 | self.options = options 15 | 16 | def allowed(self, request): 17 | if self.whitelisted(request): 18 | return True 19 | elif self.blacklisted(request): 20 | return False 21 | return True 22 | 23 | def whitelisted(self, request): 24 | return False 25 | 26 | def blacklisted(self, request): 27 | return False 28 | 29 | def client_identifier(self, request): 30 | if request.user.is_authenticated: 31 | ident = request.user.username 32 | else: 33 | ident = request.META.get("REMOTE_ADDR", None) 34 | if not ident: 35 | return '' 36 | 37 | ident = "%s,%s" % (self.res.__class__.__name__, ident) 38 | return ident 39 | 40 | def cache_get(self, key, default=None): 41 | return cache.get(key, default) 42 | 43 | def cache_set(self, key, value, expires): 44 | return cache.set(key, value, expires) 45 | 46 | def cache_key(self, request): 47 | if not "key_prefix" in self.options: 48 | return self.client_identifier() 49 | key = "%s:%s" % (self.options.get("key_prefix"), 50 | self.client_identifier()) 51 | return key 52 | 53 | 54 | class Interval(Limiter): 55 | """ 56 | This rate limiter strategy throttles the application by enforcing a 57 | minimum interval (by default, 1 second) between subsequent allowed HTTP 58 | requests. 59 | 60 | ex:: 61 | from webmachine import Resource 62 | from webmachine.throttle import Interval 63 | 64 | class MyResource(Resource): 65 | ... 66 | 67 | def forbidden(self, req, resp): 68 | return Interval(self).allowed(req) 69 | """ 70 | 71 | def allowed(self, request): 72 | t1 = time.time() 73 | key = self.cache_key(request) 74 | t0 = self.cache_get(key) 75 | allowed = not t0 or (t1 - t0) >= self.min_interval() 76 | try: 77 | self.cache_set(key, t1) 78 | except: 79 | return True 80 | return allowed 81 | 82 | def min_interval(self): 83 | return "min" in self.options and self.options.get("min") or 1 84 | 85 | class TimeWindow(Limiter): 86 | """ 87 | Return ```true``` if fewer than maximum number of requests 88 | permitted for the current window of time have been made. 89 | """ 90 | 91 | def allowed(self, request): 92 | t1 = time.time() 93 | key = self.cache_key(request) 94 | count = int(self.cache_get(key) or 0) 95 | allowed = count <= self.max_per_window() 96 | try: 97 | self.cache_set(key, t1) 98 | except: 99 | return True 100 | return allowed 101 | 102 | def max_per_window(self): 103 | raise NotImplementedError 104 | 105 | 106 | class Daily(TimeWindow): 107 | """ 108 | This rate limiter strategy throttles the application by defining a 109 | maximum number of allowed HTTP requests per day (by default, 86,400 110 | requests per 24 hours, which works out to an average of 1 request per 111 | second). 112 | 113 | Note that this strategy doesn't use a sliding time window, but rather 114 | tracks requests per calendar day. This means that the throttling counter 115 | is reset at midnight (according to the server's local timezone) every 116 | night. 117 | 118 | ex:: 119 | from webmachine import Resource 120 | from webmachine.throttle import Daily 121 | 122 | class MyResource(Resource): 123 | ... 124 | 125 | def forbidden(self, req, resp): 126 | return Daily(self).allowed(req) 127 | """ 128 | 129 | def max_per_window(self): 130 | return "max" in self.options and self.options.get(max) or 86400 131 | 132 | def cache_key(self, request): 133 | return "%s:%s" % (super(Daily, self).cache_key(request), 134 | time.strftime('%Y-%m-%d')) 135 | 136 | class Hourly(TimeWindow): 137 | """ 138 | This rate limiter strategy throttles the application by defining a 139 | maximum number of allowed HTTP requests per hour (by default, 3,600 140 | requests per 60 minutes, which works out to an average of 1 request per 141 | second). 142 | 143 | Note that this strategy doesn't use a sliding time window, but rather 144 | tracks requests per distinct hour. This means that the throttling 145 | counter is reset every hour on the hour (according to the server's local 146 | timezone). 147 | 148 | ex:: 149 | from webmachine import Resource 150 | from webmachine.throttle import Hourly 151 | 152 | class MyResource(Resource): 153 | ... 154 | 155 | def forbidden(self, req, resp): 156 | return Hourly(self).allowed(req) 157 | """ 158 | 159 | def max_per_window(self): 160 | return "max" in self.options and self.options.get(max) or 3600 161 | 162 | def cache_key(self, request): 163 | return "%s:%s" % (super(Daily, self).cache_key(request), 164 | time.strftime('%Y-%m-%dT%H')) 165 | 166 | -------------------------------------------------------------------------------- /webmachine/util/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-apipoint released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import random 7 | import time 8 | 9 | CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)' 10 | def keygen(len): 11 | return ''.join([random.choice(CHARS) for i in range(len)]) 12 | 13 | def generate_timestamp(): 14 | """Get seconds since epoch (UTC).""" 15 | return int(time.time()) 16 | 17 | def generate_random(length=8): 18 | """Generate pseudorandom number.""" 19 | return ''.join([str(random.randint(0, 9)) for i in range(length)]) 20 | 21 | def coerce_put_post(request): 22 | """ 23 | Django doesn't particularly understand REST. 24 | In case we send data over PUT, Django won't 25 | actually look at the data and load it. We need 26 | to twist its arm here. 27 | 28 | The try/except abominiation here is due to a bug 29 | in mod_python. This should fix it. 30 | 31 | function from Piston 32 | """ 33 | if request.method == "PUT": 34 | # Bug fix: if _load_post_and_files has already been called, for 35 | # example by middleware accessing request.POST, the below code to 36 | # pretend the request is a POST instead of a PUT will be too late 37 | # to make a difference. Also calling _load_post_and_files will result 38 | # in the following exception: 39 | # AttributeError: You cannot set the upload handlers after the upload has been processed. 40 | # The fix is to check for the presence of the _post field which is set 41 | # the first time _load_post_and_files is called (both by wsgi.py and 42 | # modpython.py). If it's set, the request has to be 'reset' to redo 43 | # the query value parsing in POST mode. 44 | if hasattr(request, '_post'): 45 | del request._post 46 | del request._files 47 | 48 | try: 49 | request.method = "POST" 50 | request._load_post_and_files() 51 | request.method = "PUT" 52 | except AttributeError: 53 | request.META['REQUEST_METHOD'] = 'POST' 54 | request._load_post_and_files() 55 | request.META['REQUEST_METHOD'] = 'PUT' 56 | 57 | request.PUT = request.POST 58 | 59 | def serialize_list(value): 60 | if value is None: 61 | return 62 | if isinstance(value, unicode): 63 | return str(value) 64 | elif isinstance(value, str): 65 | return value 66 | else: 67 | return ', '.join([str(v) for v in value]) 68 | 69 | -------------------------------------------------------------------------------- /webmachine/util/const.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | VERIFIER_SIZE = 16 9 | KEY_SIZE = 32 10 | SECRET_SIZE = 32 11 | 12 | # token types 13 | TOKEN_REQUEST = 2 14 | TOKEN_ACCESS = 1 15 | 16 | TOKEN_TYPES = ( 17 | (TOKEN_ACCESS, _("Access")), 18 | (TOKEN_REQUEST, _("Request")) 19 | ) 20 | 21 | # consumer states 22 | PENDING = 1 23 | ACCEPTED = 2 24 | CANCELED = 3 25 | REJECTED = 4 26 | 27 | CONSUMER_STATES = ( 28 | (PENDING, _('Pending')), 29 | (ACCEPTED, _('Accepted')), 30 | (CANCELED, _('Canceled')), 31 | (REJECTED, _('Rejected')), 32 | ) 33 | -------------------------------------------------------------------------------- /webmachine/wmtrace.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | 6 | import os 7 | import os.path 8 | from glob import iglob 9 | 10 | try: 11 | import json 12 | except ImportError: 13 | import django.utils.simplejson as json 14 | 15 | from django.template.loader import render_to_string 16 | from django.utils.encoding import smart_str 17 | from django.views import static 18 | from webmachine import Resource 19 | 20 | class WMTraceResource(Resource): 21 | 22 | def __init__(self, path="/tmp"): 23 | if path.endswith("/"): 24 | path = path[:-1] 25 | self.path = os.path.abspath(path) 26 | 27 | def trace_list_html(self, req, resp): 28 | files = [os.path.basename(f).split("wmtrace-")[1] for f in \ 29 | iglob("%s/wmtrace-*.*" % self.path)] 30 | return render_to_string("wm/wmtrace_list.html", { 31 | "path": self.path, 32 | "files": files 33 | }) 34 | 35 | def trace_html(self, req, resp): 36 | fname = req.url_kwargs["file"] 37 | fname = os.path.join(self.path, "wmtrace-%s" % fname) 38 | with open(fname, "r+b") as f: 39 | return render_to_string("wm/wmtrace.html", { 40 | "fname": fname, 41 | "trace": f.read() 42 | }) 43 | 44 | def to_html(self, req, resp): 45 | if "file" in req.url_kwargs: 46 | return self.trace_html(req, resp) 47 | return self.trace_list_html(req, resp) 48 | 49 | 50 | def get_urls(self): 51 | from django.conf.urls.defaults import patterns, url 52 | media_path = os.path.abspath(os.path.join(__file__, "..", 53 | "media")) 54 | print media_path 55 | urlpatterns = patterns('', 56 | url(r'wmtrace-(?P.+)$', self, name="wmtrace"), 57 | url(r'^static/(?P.*)', static.serve, { 58 | 'document_root': media_path, 59 | 'show_indexes': False 60 | }), 61 | url(r'$', self, name="wmtrace_list"), 62 | 63 | ) 64 | 65 | return urlpatterns 66 | 67 | -------------------------------------------------------------------------------- /webmachine/wrappers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 - 2 | # 3 | # This file is part of dj-webmachine released under the MIT license. 4 | # See the NOTICE for more information. 5 | import re 6 | 7 | from django.core.handlers.wsgi import WSGIRequest 8 | from django.http import HttpResponse 9 | from webob import Request 10 | from webob.descriptors import * 11 | from webob.datetime_utils import * 12 | from webob.headers import ResponseHeaders 13 | 14 | 15 | _PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I) 16 | _OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I) 17 | 18 | class WMRequest(WSGIRequest, Request): 19 | 20 | environ = None 21 | path = None 22 | method = None 23 | META = None 24 | 25 | def __init__(self, environ, *args, **kwargs): 26 | Request.__init__(self, environ) 27 | WSGIRequest.__init__(self, environ) 28 | 29 | # add path args args to the request 30 | self.url_args = args or [] 31 | self.url_kwargs = kwargs or {} 32 | 33 | webob_POST = Request.POST 34 | webob_WGET = Request.GET 35 | 36 | @property 37 | def str_POST(self): 38 | 39 | clength = self.environ.get('CONTENT_LENGTH') 40 | try: 41 | return super(WMRequest, self).str_POST 42 | finally: 43 | self.environ['CONTENT_LENGTH'] = clength 44 | self._seek_input() 45 | 46 | def _load_post_and_files(self): 47 | try: 48 | return WSGIRequest._load_post_and_files(self) 49 | finally: 50 | # "Resetting" the input so WebOb will read it: 51 | self._seek_input() 52 | 53 | def _seek_input(self): 54 | if "wsgi.input" in self.environ: 55 | try: 56 | self.environ['wsgi.input'].seek(0) 57 | except AttributeError: 58 | pass 59 | 60 | class WMResponse(HttpResponse): 61 | """ Add some properties to HttpResponse """ 62 | 63 | status_code = 200 64 | 65 | default_content_type = 'text/html' 66 | default_charset = 'UTF-8' 67 | unicode_errors = 'strict' 68 | default_conditional_response = False 69 | 70 | def __init__(self, content='', mimetype=None, status=None, 71 | content_type=None, request=None): 72 | if isinstance(status, basestring): 73 | (status_code, status_reason) = status.split(" ", 1) 74 | status_code = int(status_code) 75 | self.status_reason = status_reason or None 76 | else: 77 | status_code = status 78 | self.status_reason = None 79 | 80 | self.request = request 81 | self._headerlist = [] 82 | 83 | HttpResponse.__init__(self, content=content, 84 | status=status_code, content_type=content_type) 85 | 86 | 87 | 88 | def _headerlist__get(self): 89 | """ 90 | The list of response headers 91 | """ 92 | return self._headers.values() 93 | 94 | def _headerlist__set(self, value): 95 | self._headers = {} 96 | if not isinstance(value, list): 97 | if hasattr(value, 'items'): 98 | value = value.items() 99 | value = list(value) 100 | 101 | headers = ResponseHeaders.view_list(self.headerlist) 102 | for hname in headers.keys(): 103 | self._headers[hname.lower()] = (hname, headers[hname]) 104 | self._headerlist = value 105 | 106 | 107 | def _headerlist__del(self): 108 | self.headerlist = [] 109 | self._headers = {} 110 | 111 | headerlist = property(_headerlist__get, _headerlist__set, _headerlist__del, doc=_headerlist__get.__doc__) 112 | 113 | def __setitem__(self, header, value): 114 | header, value = self._convert_to_ascii(header, value) 115 | self._headers[header.lower()] = (header, value) 116 | 117 | def __delitem__(self, header): 118 | try: 119 | del self._headers[header.lower()] 120 | except KeyError: 121 | return 122 | 123 | def __getitem__(self, header): 124 | return self._headers[header.lower()][1] 125 | 126 | allow = list_header('Allow', '14.7') 127 | ## FIXME: I realize response.vary += 'something' won't work. It should. 128 | ## Maybe for all listy headers. 129 | vary = list_header('Vary', '14.44') 130 | 131 | content_length = converter( 132 | header_getter('Content-Length', '14.17'), 133 | parse_int, serialize_int, 'int') 134 | 135 | content_encoding = header_getter('Content-Encoding', '14.11') 136 | content_language = list_header('Content-Language', '14.12') 137 | content_location = header_getter('Content-Location', '14.14') 138 | content_md5 = header_getter('Content-MD5', '14.14') 139 | # FIXME: a special ContentDisposition type would be nice 140 | content_disposition = header_getter('Content-Disposition', '19.5.1') 141 | 142 | accept_ranges = header_getter('Accept-Ranges', '14.5') 143 | content_range = converter( 144 | header_getter('Content-Range', '14.16'), 145 | parse_content_range, serialize_content_range, 'ContentRange object') 146 | 147 | date = date_header('Date', '14.18') 148 | expires = date_header('Expires', '14.21') 149 | last_modified = date_header('Last-Modified', '14.29') 150 | 151 | etag = converter( 152 | header_getter('ETag', '14.19'), 153 | parse_etag_response, serialize_etag_response, 'Entity tag') 154 | 155 | location = header_getter('Location', '14.30') 156 | pragma = header_getter('Pragma', '14.32') 157 | age = converter( 158 | header_getter('Age', '14.6'), 159 | parse_int_safe, serialize_int, 'int') 160 | 161 | retry_after = converter( 162 | header_getter('Retry-After', '14.37'), 163 | parse_date_delta, serialize_date_delta, 'HTTP date or delta seconds') 164 | 165 | server = header_getter('Server', '14.38') 166 | 167 | def _convert_to_ascii(self, header, value): 168 | def convert(s): 169 | try: 170 | return s.decode('ascii') 171 | except AttributeError: 172 | return s 173 | return convert(header), convert(value) 174 | 175 | # 176 | # charset 177 | # 178 | 179 | def _charset__get(self): 180 | """ 181 | Get/set the charset (in the Content-Type) 182 | """ 183 | header = self._headers.get('content-type') 184 | if not header: 185 | return None 186 | match = CHARSET_RE.search(header[1]) 187 | if match: 188 | return match.group(1) 189 | return None 190 | 191 | def _charset__set(self, charset): 192 | if charset is None: 193 | del self.charset 194 | return 195 | try: 196 | hname, header = self._headers.pop('content-type') 197 | except KeyError: 198 | raise AttributeError( 199 | "You cannot set the charset when no content-type is defined") 200 | match = CHARSET_RE.search(header) 201 | if match: 202 | header = header[:match.start()] + header[match.end():] 203 | header += '; charset=%s' % charset 204 | self._headers['content-type'] = hname, header 205 | 206 | def _charset__del(self): 207 | try: 208 | hname, header = self._headers.pop('content-type') 209 | except KeyError: 210 | # Don't need to remove anything 211 | return 212 | match = CHARSET_RE.search(header) 213 | if match: 214 | header = header[:match.start()] + header[match.end():] 215 | self[hname] = header 216 | 217 | charset = property(_charset__get, _charset__set, _charset__del, doc=_charset__get.__doc__) 218 | 219 | 220 | # 221 | # content_type 222 | # 223 | 224 | def _content_type__get(self): 225 | """ 226 | Get/set the Content-Type header (or None), *without* the 227 | charset or any parameters. 228 | 229 | If you include parameters (or ``;`` at all) when setting the 230 | content_type, any existing parameters will be deleted; 231 | otherwise they will be preserved. 232 | """ 233 | header = self._headers.get('content-type') 234 | 235 | if not header: 236 | return None 237 | return header[1].split(';', 1)[0] 238 | 239 | def _content_type__set(self, value): 240 | if ';' not in value: 241 | if 'content-type' in self._headers: 242 | header = self._headers.get('content-type') 243 | if ';' in header[1]: 244 | params = header[1].split(';', 1)[1] 245 | value += ';' + params 246 | self['Content-Type'] = value 247 | 248 | def _content_type__del(self): 249 | try: 250 | del self._headers['content-type'] 251 | except KeyError: 252 | pass 253 | 254 | content_type = property(_content_type__get, _content_type__set, 255 | _content_type__del, doc=_content_type__get.__doc__) 256 | 257 | 258 | # 259 | # content_type_params 260 | # 261 | 262 | def _content_type_params__get(self): 263 | """ 264 | A dictionary of all the parameters in the content type. 265 | 266 | (This is not a view, set to change, modifications of the dict would not be 267 | applied otherwise) 268 | """ 269 | if not 'content-type' in self._headers: 270 | return {} 271 | 272 | params = self._headers.get('content-type') 273 | if ';' not in params[1]: 274 | return {} 275 | params = params[1].split(';', 1)[1] 276 | result = {} 277 | for match in _PARAM_RE.finditer(params): 278 | result[match.group(1)] = match.group(2) or match.group(3) or '' 279 | return result 280 | 281 | def _content_type_params__set(self, value_dict): 282 | if not value_dict: 283 | del self.content_type_params 284 | return 285 | params = [] 286 | for k, v in sorted(value_dict.items()): 287 | if not _OK_PARAM_RE.search(v): 288 | ## FIXME: I'm not sure what to do with "'s in the parameter value 289 | ## I think it might be simply illegal 290 | v = '"%s"' % v.replace('"', '\\"') 291 | params.append('; %s=%s' % (k, v)) 292 | ct = self._headers.pop('content-type') 293 | if not ct: 294 | ct = '' 295 | else: 296 | ct = ct[1].split(';', 1)[0] 297 | ct += ''.join(params) 298 | self._headers['content-type'] = 'Content-Type', ct 299 | 300 | def _content_type_params__del(self, value): 301 | try: 302 | header = self._headers['content-type'] 303 | except KeyError: 304 | return 305 | 306 | self._headers['content-type'] = header[0], header[1].split(';', 1)[0] 307 | 308 | content_type_params = property( 309 | _content_type_params__get, 310 | _content_type_params__set, 311 | _content_type_params__del, 312 | doc=_content_type_params__get.__doc__ 313 | ) 314 | 315 | --------------------------------------------------------------------------------