diff options
| author | 2011-12-08 02:08:35 +0100 | |
|---|---|---|
| committer | 2011-12-08 02:08:35 +0100 | |
| commit | 30ba647fe479d86c3d7bac71908ee56ec80eb928 (patch) | |
| tree | 765c5e0295d81f712e070da774d591561848f34b | |
| parent | httprequest: encode('utf_8') for unicode post (diff) | |
| parent | updated bottle.py (diff) | |
| download | pyload-30ba647fe479d86c3d7bac71908ee56ec80eb928.tar.xz | |
Merge
| -rw-r--r-- | README | 96 | ||||
| -rw-r--r-- | locale/cli.pot | 116 | ||||
| -rw-r--r-- | locale/core.pot | 315 | ||||
| -rw-r--r-- | locale/django.pot | 123 | ||||
| -rw-r--r-- | locale/gui.pot | 30 | ||||
| -rw-r--r-- | locale/setup.pot | 254 | ||||
| -rw-r--r-- | module/Api.py | 3 | ||||
| -rw-r--r-- | module/common/pylgettext.py | 62 | ||||
| -rw-r--r-- | module/lib/bottle.py | 2026 | ||||
| -rw-r--r-- | module/remote/thriftbackend/pyload.thrift | 57 | ||||
| -rwxr-xr-x | module/remote/thriftbackend/thriftgen/pyload/Pyload-remote | 58 | ||||
| -rw-r--r-- | module/remote/thriftbackend/thriftgen/pyload/Pyload.py | 646 | ||||
| -rw-r--r-- | module/remote/thriftbackend/thriftgen/pyload/constants.py | 2 | ||||
| -rw-r--r-- | module/remote/thriftbackend/thriftgen/pyload/ttypes.py | 108 | ||||
| -rw-r--r-- | module/setup.py | 165 | ||||
| -rw-r--r-- | module/web/pyload_app.py | 2 | ||||
| -rw-r--r-- | module/web/webinterface.py | 6 | ||||
| -rw-r--r-- | pavement.py | 5 | ||||
| -rwxr-xr-x | pyLoadCli.py | 10 | ||||
| -rwxr-xr-x | pyLoadCore.py | 5 | ||||
| -rwxr-xr-x | pyLoadGui.py | 6 | 
21 files changed, 2406 insertions, 1689 deletions
| @@ -1,19 +1,4 @@ -   ?IIIIIIII77777:~~.                      NM                               M    - +III777777$$$$$7~~~~,                     NM                               M    - III777777$$$$$$+~~~==  MMMMMMM  8M     M  NM        MMI      OMM$      MMO M    - I777777++++++=.~~====  M     MM 8M     M  NM     7M    MN        M   MD    M    - 77777 ~~~~~~~~~======  M     NM 8M     M  NM     MO     M    $DDDM  NM     M    - 77777~~~~~~~=========  M     MM 8M     M  NM     M8     M  MM    M  NM     M    -  777$~~~~~==========   MNMZ8MM  +MMMMM?M  NM     $M    MM  MM    M   MZ    M    -      ~~~==,,,,,,       M               M  NMMMMMN  7MMO      MMM=     IMMD      -      ~======= =~       M               M                                        -      ~======~ =                    DMMO                                         -       ~=======                                                                  -        :=====                                                                   -         :===                                                                    -          ,=,                                                                  -  Description  =========== @@ -23,47 +8,82 @@ It supports link decryption as well as all important container formats.  pyLoad is written entirely in Python and is currently under heavy development. -To read the newest info, get the latest version, find help or contribute -to the wiki, visit <http://pyload.org/> +For news, downloads, wiki, forum and further information visit http://pyload.org/  To report bugs, suggest features, ask a question, get the developer version -or help us out, visit <http://bitbucket.org/spoob/pyload/> +or help us out, visit http://bitbucket.org/spoob/pyload/ + +Documentation about extending pyLoad can be found at http://docs.pyload.org or join us at #pyload on irc.freenode.net -Requirements +Dependencies  ============ -pycrypto: RSDF/CCF/DLC support -pycurl: lower memory footprint while downloading -pyqt4: for the GUI -tesseract, PIL: Captcha recognition +You need at least python 2.5 to run pyLoad and all of these required libaries. +They should be automatically installed when using pip install. +The prebuilt pyload packages also install these dependencies or have them included, so manuall install +is only needed when installing pyLoad from source. + +Required +-------- + +- pycurl a.k.a python-curl +- jinja2 +- beaker +- thrift +- simplejson (for python <= 2.6) + +Some plugins require additional packages, only install these when needed. + +Optional +-------- + +- pycrypto: RSDF/CCF/DLC support +- tesseract, python-pil a.k.a python-imaging: Automatic captcha recognition for a small amount of plugins +- jsengine (spidermonkey, ossp-js, pyv8, rhino): Used for several hoster, ClickNLoad +- feedparser +- BeautifulSoup +- pyOpenSSL: For SSL connection  First start  =========== -First, run -$ python pyLoadCore.py -Follow the instructions of the setup assistent. +Note: If you installed pyload via package-manager `python pyLoadCore.py` is probably equivalent to `pyLoadCore` + +Run:: + +    python pyLoadCore.py + +and follow the instructions of the setup assistent. + +For a list of options use:: + +    python pyLoadCore.py -h  Configuration -=========== +============= -After the first start you can configurate pyLoad with the webinterface or the GUI. -Additionally you could simply edit the config files located in your pyLoad home dir(defaults to: ~/.pyload) +After finishing the setup assistent pyLoad is ready to use and more configuration can be done via webinterface. +Additionally you could simply edit the config files located in your pyLoad home dir (defaults to: ~/.pyload)  with your favorite editor and edit the appropriate options. For a short description of -the options take a look at <http://pyload.org/configuration>. +the options take a look at http://pyload.org/configuration. + +To restart the configure assistent run:: + +    python pyLoadCore.py -s  Adding downloads -================ +---------------- + +To start the CLI and connect to a local server, run:: + +    python pyLoadCli.py -l -To start the CLI and connect to a local server, run -$ python pyLoadCli.py -l +for more options refer to:: -If you want to connect from graphical enviroment, you can also use the GUI: -$ python pyLoadGui.py +    python pyLoadCli.py -h -Or access the address you setted for the webinterface with your webbrowser. +The webinterface can be accessed when pointing your webbrowser to the ip and configured port, defaults to http://localhost:8000  Notes  ===== -For more information, see -<http://pyload.org/> +For more information, see http://pyload.org/ diff --git a/locale/cli.pot b/locale/cli.pot index b2d4f50a1..646c6c70e 100644 --- a/locale/cli.pot +++ b/locale/cli.pot @@ -8,7 +8,7 @@ msgid ""  msgstr ""  "Project-Id-Version: pyLoad 0.4.9\n"  "Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" -"POT-Creation-Date: 2011-10-11 18:59+0200\n" +"POT-Creation-Date: 2011-12-07 19:21+0100\n"  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"  "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,202 +17,210 @@ msgstr ""  "Content-Type: text/plain; charset=UTF-8\n"  "Content-Transfer-Encoding: 8bit\n" -#: pyLoadCli.py:79 pyLoadCli.py:137 +#: pyLoadCli.py:75 pyLoadCli.py:133  msgid " Command Line Interface"  msgstr "" -#: pyLoadCli.py:169 +#: pyLoadCli.py:165  #, python-format  msgid "%s Downloads:"  msgstr "" -#: pyLoadCli.py:181 +#: pyLoadCli.py:177  msgid " Speed: "  msgstr "" -#: pyLoadCli.py:181 +#: pyLoadCli.py:177  msgid " Size: "  msgstr "" -#: pyLoadCli.py:182 +#: pyLoadCli.py:178  msgid " Finished in: "  msgstr "" -#: pyLoadCli.py:183 +#: pyLoadCli.py:179  msgid " ID: "  msgstr "" -#: pyLoadCli.py:188 +#: pyLoadCli.py:184  msgid "waiting: "  msgstr "" -#: pyLoadCli.py:195 pyLoadCli.py:197 -msgid "Status: " +#: pyLoadCli.py:191 pyLoadCli.py:193 +msgid "Status:"  msgstr "" -#: pyLoadCli.py:200 -msgid " total Speed: " +#: pyLoadCli.py:191 +msgid "paused"  msgstr "" -#: pyLoadCli.py:201 -msgid " Files in queue: " +#: pyLoadCli.py:193 +msgid "running"  msgstr "" -#: pyLoadCli.py:201 -msgid " Total: " +#: pyLoadCli.py:196 +msgid "total Speed"  msgstr "" -#: pyLoadCli.py:207 +#: pyLoadCli.py:196 +msgid "Files in queue" +msgstr "" + +#: pyLoadCli.py:197 +msgid "Total" +msgstr "" + +#: pyLoadCli.py:203  msgid "Menu:"  msgstr "" -#: pyLoadCli.py:209 +#: pyLoadCli.py:205  msgid " Add Links"  msgstr "" -#: pyLoadCli.py:210 +#: pyLoadCli.py:206  msgid " Manage Queue"  msgstr "" -#: pyLoadCli.py:211 +#: pyLoadCli.py:207  msgid " Manage Collector"  msgstr "" -#: pyLoadCli.py:212 +#: pyLoadCli.py:208  msgid " (Un)Pause Server"  msgstr "" -#: pyLoadCli.py:213 +#: pyLoadCli.py:209  msgid " Kill Server"  msgstr "" -#: pyLoadCli.py:214 +#: pyLoadCli.py:210  msgid " Quit"  msgstr "" -#: pyLoadCli.py:293 pyLoadCli.py:300 +#: pyLoadCli.py:289 pyLoadCli.py:296  msgid "Please use this syntax: add <Package name> <link> <link2> ..."  msgstr "" -#: pyLoadCli.py:319 +#: pyLoadCli.py:315  #, python-format  msgid "Checking %d links:"  msgstr "" -#: pyLoadCli.py:328 +#: pyLoadCli.py:324  msgid "File does not exists."  msgstr "" -#: pyLoadCli.py:389 +#: pyLoadCli.py:385  msgid "pyLoad was terminated"  msgstr "" -#: pyLoadCli.py:447 +#: pyLoadCli.py:443  msgid "Prints server status"  msgstr "" -#: pyLoadCli.py:448 +#: pyLoadCli.py:444  msgid "Prints downloads in queue"  msgstr "" -#: pyLoadCli.py:449 +#: pyLoadCli.py:445  msgid "Prints downloads in collector"  msgstr "" -#: pyLoadCli.py:450 +#: pyLoadCli.py:446  msgid "Adds package to queue"  msgstr "" -#: pyLoadCli.py:451 +#: pyLoadCli.py:447  msgid "Adds package to collector"  msgstr "" -#: pyLoadCli.py:452 +#: pyLoadCli.py:448  msgid "Delete Files from Queue/Collector"  msgstr "" -#: pyLoadCli.py:453 +#: pyLoadCli.py:449  msgid "Delete Packages from Queue/Collector"  msgstr "" -#: pyLoadCli.py:454 +#: pyLoadCli.py:450  msgid "Move Packages from Queue to Collector or vice versa"  msgstr "" -#: pyLoadCli.py:455 +#: pyLoadCli.py:451  msgid "Restart files"  msgstr "" -#: pyLoadCli.py:456 +#: pyLoadCli.py:452  msgid "Restart packages"  msgstr "" -#: pyLoadCli.py:457 +#: pyLoadCli.py:453  msgid "Check online status, works with local container"  msgstr "" -#: pyLoadCli.py:458 +#: pyLoadCli.py:454  msgid "Checks online status of a container file"  msgstr "" -#: pyLoadCli.py:459 +#: pyLoadCli.py:455  msgid "Pause the server"  msgstr "" -#: pyLoadCli.py:460 +#: pyLoadCli.py:456  msgid "continue downloads"  msgstr "" -#: pyLoadCli.py:461 +#: pyLoadCli.py:457  msgid "Toggle pause/unpause"  msgstr "" -#: pyLoadCli.py:462 +#: pyLoadCli.py:458  msgid "kill server"  msgstr "" -#: pyLoadCli.py:464 +#: pyLoadCli.py:460  msgid "List of commands:"  msgstr "" -#: pyLoadCli.py:477 +#: pyLoadCli.py:473  msgid "Couldn't write user config file"  msgstr "" -#: pyLoadCli.py:549 +#: pyLoadCli.py:548  msgid "You need py-openssl to connect to this pyLoad Core."  msgstr "" -#: pyLoadCli.py:556 +#: pyLoadCli.py:555  msgid "Address: "  msgstr "" -#: pyLoadCli.py:557 +#: pyLoadCli.py:556  msgid "Port: "  msgstr "" -#: pyLoadCli.py:558 +#: pyLoadCli.py:557  msgid "Username: "  msgstr "" -#: pyLoadCli.py:562 +#: pyLoadCli.py:561  msgid "Password: "  msgstr "" -#: pyLoadCli.py:567 pyLoadCli.py:576 +#: pyLoadCli.py:566 pyLoadCli.py:575  msgid "Login data is wrong."  msgstr "" -#: pyLoadCli.py:569 pyLoadCli.py:578 +#: pyLoadCli.py:568 pyLoadCli.py:577  #, python-format  msgid "Could not establish connection to %(addr)s:%(port)s."  msgstr "" -#: pyLoadCli.py:581 +#: pyLoadCli.py:580  msgid "You need py-openssl to connect to this pyLoad core."  msgstr "" -#: pyLoadCli.py:583 +#: pyLoadCli.py:582  msgid "Interactive mode ignored since you passed some commands."  msgstr "" diff --git a/locale/core.pot b/locale/core.pot index 6b38ef5e8..546f0e4d3 100644 --- a/locale/core.pot +++ b/locale/core.pot @@ -8,7 +8,7 @@ msgid ""  msgstr ""  "Project-Id-Version: pyLoad 0.4.9\n"  "Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" -"POT-Creation-Date: 2011-10-11 18:59+0200\n" +"POT-Creation-Date: 2011-12-07 19:21+0100\n"  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"  "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,120 +17,120 @@ msgstr ""  "Content-Type: text/plain; charset=UTF-8\n"  "Content-Transfer-Encoding: 8bit\n" -#: pyLoadCore.py:174 +#: pyLoadCore.py:185  msgid "Received Quit signal"  msgstr "" -#: pyLoadCore.py:288 +#: pyLoadCore.py:301  #, python-format  msgid "pyLoad already running with pid %s"  msgstr "" -#: pyLoadCore.py:302 +#: pyLoadCore.py:315  #, python-format  msgid "Failed changing group: %s"  msgstr "" -#: pyLoadCore.py:312 +#: pyLoadCore.py:325  #, python-format  msgid "Failed changing user: %s"  msgstr "" -#: pyLoadCore.py:314 +#: pyLoadCore.py:327  msgid "folder for logs"  msgstr "" -#: pyLoadCore.py:325 +#: pyLoadCore.py:338  msgid "Starting"  msgstr "" -#: pyLoadCore.py:326 +#: pyLoadCore.py:339  #, python-format  msgid "Using home directory: %s"  msgstr "" -#: pyLoadCore.py:332 +#: pyLoadCore.py:348  msgid "pycrypto to decode container files"  msgstr "" -#: pyLoadCore.py:335 +#: pyLoadCore.py:351  msgid "folder for temporary files"  msgstr "" -#: pyLoadCore.py:340 +#: pyLoadCore.py:356  msgid "folder for downloads"  msgstr "" -#: pyLoadCore.py:343 +#: pyLoadCore.py:359  msgid "OpenSSL for secure connection"  msgstr "" -#: pyLoadCore.py:347 +#: pyLoadCore.py:363  msgid "Moving old user config to DB"  msgstr "" -#: pyLoadCore.py:350 +#: pyLoadCore.py:366  msgid "Please check your logindata with ./pyLoadCore.py -u"  msgstr "" -#: pyLoadCore.py:353 +#: pyLoadCore.py:369  msgid "All links removed"  msgstr "" -#: pyLoadCore.py:378 +#: pyLoadCore.py:400  #, python-format  msgid "Downloadtime: %s"  msgstr "" -#: pyLoadCore.py:390 +#: pyLoadCore.py:410  #, python-format  msgid "Free space: %s"  msgstr "" -#: pyLoadCore.py:410 +#: pyLoadCore.py:430  msgid "Activating Accounts..."  msgstr "" -#: pyLoadCore.py:416 +#: pyLoadCore.py:436  msgid "Activating Plugins..."  msgstr "" -#: pyLoadCore.py:419 +#: pyLoadCore.py:439  msgid "pyLoad is up and running"  msgstr "" -#: pyLoadCore.py:438 +#: pyLoadCore.py:458  msgid "restarting pyLoad"  msgstr "" -#: pyLoadCore.py:442 +#: pyLoadCore.py:462  msgid "pyLoad quits"  msgstr "" -#: pyLoadCore.py:499 +#: pyLoadCore.py:519  #, python-format  msgid "Install %s"  msgstr "" -#: pyLoadCore.py:535 +#: pyLoadCore.py:555  #, python-format  msgid "could not find %(desc)s: %(name)s"  msgstr "" -#: pyLoadCore.py:537 +#: pyLoadCore.py:557  #, python-format  msgid "could not create %(desc)s: %(name)s"  msgstr "" -#: pyLoadCore.py:551 +#: pyLoadCore.py:578  msgid "shutting down..."  msgstr "" -#: pyLoadCore.py:568 +#: pyLoadCore.py:595  msgid "error while shutting down"  msgstr "" -#: pyLoadCore.py:634 +#: pyLoadCore.py:659  msgid "killed pyLoad from Terminal"  msgstr "" @@ -140,84 +140,72 @@ msgid ""  "rhino"  msgstr "" -#: module/common/MultiHoster.py:60 -msgid "No Hoster loaded" -msgstr "" -  #: module/remote/ThriftBackend.py:39  msgid "Using SSL ThriftBackend"  msgstr "" -#: module/remote/XMLRPCBackend.py:28 -msgid "Using SSL XMLRPCBackend" -msgstr "" - -#: module/remote/XMLRPCBackend.py:32 -msgid "SSL Certificates not found, fallback to auth XMLRPC server" -msgstr "" - -#: module/remote/RemoteManager.py:32 +#: module/remote/RemoteManager.py:35  #, python-format  msgid "Remote backend error: %s"  msgstr "" -#: module/remote/RemoteManager.py:64 +#: module/remote/RemoteManager.py:82  #, python-format  msgid "Starting %(name)s: %(addr)s:%(port)s"  msgstr "" -#: module/remote/RemoteManager.py:66 +#: module/remote/RemoteManager.py:84  #, python-format  msgid "Failed loading backend %(name)s | %(error)s"  msgstr "" -#: module/ThreadManager.py:135 +#: module/ThreadManager.py:137  #, python-format  msgid "Reconnect Failed: %s"  msgstr "" -#: module/ThreadManager.py:174 +#: module/ThreadManager.py:176  msgid "Reconnect script not found!"  msgstr "" -#: module/ThreadManager.py:180 +#: module/ThreadManager.py:182  msgid "Starting reconnect"  msgstr "" -#: module/ThreadManager.py:194 +#: module/ThreadManager.py:196  msgid "Failed executing reconnect script!"  msgstr "" -#: module/ThreadManager.py:206 +#: module/ThreadManager.py:208  #, python-format  msgid "Reconnected, new IP: %s"  msgstr "" -#: module/ThreadManager.py:286 +#: module/ThreadManager.py:288  msgid "Not enough space left on device"  msgstr "" -#: module/HookManager.py:91 module/plugins/Hook.py:103 +#: module/HookManager.py:90 module/plugins/Hook.py:102  #, python-format  msgid "Error executing hooks: %s"  msgstr "" -#: module/HookManager.py:141 +#: module/HookManager.py:140  #, python-format  msgid "Failed activating %(name)s"  msgstr "" -#: module/HookManager.py:145 +#: module/HookManager.py:144  #, python-format  msgid "Activated plugins: %s"  msgstr "" -#: module/HookManager.py:146 +#: module/HookManager.py:145  #, python-format  msgid "Deactivate plugins: %s"  msgstr "" -#: module/CaptchaManager.py:78 +#: module/CaptchaManager.py:78 module/interaction/InteractionManager.py:82  msgid "No Client connected for captcha decrypting"  msgstr "" @@ -326,17 +314,17 @@ msgstr ""  msgid "waiting %s"  msgstr "" -#: module/Api.py:323 +#: module/Api.py:329  #, python-format  msgid "Added package %(name)s containing %(count)d links"  msgstr "" -#: module/Api.py:586 +#: module/Api.py:592  #, python-format  msgid "Added %(count)d links to package #%(package)d "  msgstr "" -#: module/plugins/crypter/SerienjunkiesOrg.py:122 +#: module/plugins/crypter/SerienjunkiesOrg.py:125  msgid "Downloadlimit reached"  msgstr "" @@ -344,71 +332,75 @@ msgstr ""  msgid "Click'N'Load: Port 9666 already in use"  msgstr "" -#: module/plugins/hooks/UnRar.py:83 +#: module/plugins/hooks/ExtractArchive.py:91  #, python-format -msgid "Directory %s does not exist!" +msgid "No %s installed"  msgstr "" -#: module/plugins/hooks/UnRar.py:94 module/plugins/hooks/UnRar.py:101 +#: module/plugins/hooks/ExtractArchive.py:93 +#: module/plugins/hooks/ExtractArchive.py:98  #, python-format -msgid "Chown/Chmod for %s failed" +msgid "Could not activate %s"  msgstr "" -#: module/plugins/hooks/UnRar.py:95 module/plugins/hooks/UnRar.py:102 -#, python-format -msgid "Exception: %s" +#: module/plugins/hooks/ExtractArchive.py:103 +msgid "Activated"  msgstr "" -#: module/plugins/hooks/UnRar.py:151 -#, python-format -msgid "starting Unrar of %s" +#: module/plugins/hooks/ExtractArchive.py:105 +msgid "No Extract plugins activated"  msgstr "" -#: module/plugins/hooks/UnRar.py:159 +#: module/plugins/hooks/ExtractArchive.py:117  #, python-format -msgid "download folder %s" +msgid "Package %s queued for later extracting"  msgstr "" -#: module/plugins/hooks/UnRar.py:175 +#: module/plugins/hooks/ExtractArchive.py:142  #, python-format -msgid "Destination folder %s" +msgid "Check package %s"  msgstr "" -#: module/plugins/hooks/UnRar.py:177 +#: module/plugins/hooks/ExtractArchive.py:179  #, python-format -msgid "Creating destination folder %s" +msgid "Extract to %s"  msgstr "" -#: module/plugins/hooks/UnRar.py:186 -#, python-format -msgid "Unrar of %s failed (wrong password)" +#: module/plugins/hooks/ExtractArchive.py:198 +msgid "extracting"  msgstr "" -#: module/plugins/hooks/UnRar.py:192 -#, python-format -msgid "Unrar of %s failed (missing volume)" +#: module/plugins/hooks/ExtractArchive.py:209 +msgid "Password protected"  msgstr "" -#: module/plugins/hooks/UnRar.py:196 module/plugins/hooks/UnRar.py:214 -#, python-format -msgid "Unrar of %s ok" +#: module/plugins/hooks/ExtractArchive.py:229 +msgid "Wrong password"  msgstr "" -#: module/plugins/hooks/UnRar.py:201 module/plugins/hooks/UnRar.py:210 +#: module/plugins/hooks/ExtractArchive.py:237  #, python-format -msgid "Unrar of %s failed" +msgid "Deleting %s files"  msgstr "" -#: module/plugins/hooks/UnRar.py:205 -#, python-format -msgid "" -"Your ram amount of %s MB seems not sufficient to unrar this file. You can " -"deactivate this warning and risk instability" +#: module/plugins/hooks/ExtractArchive.py:242 +msgid "Extracting finished"  msgstr "" -#: module/plugins/hooks/UnRar.py:232 -#, python-format -msgid "Unrar of %s failed (wrong password or bad parts)" +#: module/plugins/hooks/ExtractArchive.py:249 +msgid "Archive Error" +msgstr "" + +#: module/plugins/hooks/ExtractArchive.py:251 +msgid "CRC Mismatch" +msgstr "" + +#: module/plugins/hooks/ExtractArchive.py:255 +msgid "Unknown Error" +msgstr "" + +#: module/plugins/hooks/ExtractArchive.py:307 +msgid "Setting User and Group failed"  msgstr ""  #: module/plugins/hooks/CaptchaTrader.py:71 @@ -470,53 +462,61 @@ msgstr ""  msgid "Error in %(script)s: %(error)s"  msgstr "" -#: module/plugins/hooks/UpdateManager.py:48 +#: module/plugins/hooks/UpdateManager.py:68  msgid "No Updates for pyLoad"  msgstr "" -#: module/plugins/hooks/UpdateManager.py:53 +#: module/plugins/hooks/UpdateManager.py:73  msgid "*** Plugins have been updated, please restart pyLoad ***"  msgstr "" -#: module/plugins/hooks/UpdateManager.py:55 +#: module/plugins/hooks/UpdateManager.py:75  msgid "Plugins updated and reloaded"  msgstr "" -#: module/plugins/hooks/UpdateManager.py:57 +#: module/plugins/hooks/UpdateManager.py:78  msgid "No plugin updates available"  msgstr "" -#: module/plugins/hooks/UpdateManager.py:72 +#: module/plugins/hooks/UpdateManager.py:93  #, python-format  msgid "***  New pyLoad Version %s available  ***"  msgstr "" -#: module/plugins/hooks/UpdateManager.py:73 +#: module/plugins/hooks/UpdateManager.py:94  msgid "***  Get it here: http://pyload.org/download  ***"  msgstr "" -#: module/plugins/hooks/UpdateManager.py:76 -#: module/plugins/hooks/UpdateManager.py:89 +#: module/plugins/hooks/UpdateManager.py:97 +#: module/plugins/hooks/UpdateManager.py:110  msgid "Not able to connect server for updates"  msgstr "" -#: module/plugins/hooks/UpdateManager.py:116 +#: module/plugins/hooks/UpdateManager.py:141  #, python-format  msgid "New version of %(type)s|%(name)s : %(version).2f"  msgstr "" -#: module/plugins/hooks/UpdateManager.py:125 -#: module/plugins/hooks/UpdateManager.py:130 +#: module/plugins/hooks/UpdateManager.py:150 +#: module/plugins/hooks/UpdateManager.py:155  #, python-format  msgid "Error when updating %s"  msgstr "" -#: module/plugins/hoster/OronCom.py:133 -msgid "Not enough traffic left" +#: module/plugins/hooks/UpdateManager.py:155 +msgid "Version mismatch" +msgstr "" + +#: module/plugins/hoster/BasePlugin.py:53 +msgid "Authorization required (username:password)"  msgstr ""  #: module/plugins/hoster/OronCom.py:135 -#: module/plugins/hoster/UploadedTo.py:121 +msgid "Not enough traffic left" +msgstr "" + +#: module/plugins/hoster/OronCom.py:137 +#: module/plugins/hoster/UploadedTo.py:158  msgid "Traffic exceeded"  msgstr "" @@ -551,7 +551,11 @@ msgstr ""  msgid "Wrong password for download link."  msgstr "" -#: module/plugins/hoster/UploadedTo.py:118 +#: module/plugins/hoster/UploadedTo.py:131 +msgid "API key invalid" +msgstr "" + +#: module/plugins/hoster/UploadedTo.py:155  #, python-format  msgid "%s: Not enough traffic left"  msgstr "" @@ -580,24 +584,24 @@ msgstr ""  msgid "Rapidshare: Traffic Share (direct download)"  msgstr "" -#: module/plugins/hoster/RapidshareCom.py:124 -#: module/plugins/hoster/RapidshareCom.py:190 +#: module/plugins/hoster/RapidshareCom.py:126 +#: module/plugins/hoster/RapidshareCom.py:192  msgid "Already downloading from this ip address, waiting 60 seconds"  msgstr "" -#: module/plugins/hoster/RapidshareCom.py:128 +#: module/plugins/hoster/RapidshareCom.py:130  msgid "Invalid Auth Code, download will be restarted"  msgstr "" -#: module/plugins/hoster/RapidshareCom.py:194 +#: module/plugins/hoster/RapidshareCom.py:196  msgid "RapidShareCom: No free slots"  msgstr "" -#: module/plugins/hoster/RapidshareCom.py:197 +#: module/plugins/hoster/RapidshareCom.py:199  msgid "You need a premium account for this file"  msgstr "" -#: module/plugins/hoster/RapidshareCom.py:199 +#: module/plugins/hoster/RapidshareCom.py:201  msgid "Filename reported invalid"  msgstr "" @@ -632,17 +636,17 @@ msgstr ""  msgid "LinkList could not be cleared."  msgstr "" -#: module/plugins/Plugin.py:387 +#: module/plugins/Plugin.py:381  msgid ""  "Pil and tesseract not installed and no Client connected for captcha "  "decrypting"  msgstr "" -#: module/plugins/Plugin.py:391 +#: module/plugins/Plugin.py:385  msgid "No captcha result obtained in appropiate time by any of the plugins."  msgstr "" -#: module/plugins/Plugin.py:496 module/plugins/Plugin.py:531 +#: module/plugins/Plugin.py:490 module/plugins/Plugin.py:520  #, python-format  msgid "Setting User and Group failed: %s"  msgstr "" @@ -663,46 +667,22 @@ msgstr ""  msgid "Activate direct Download in your Bitshare Account"  msgstr "" -#: module/plugins/PluginManager.py:72 -msgid "Crypter" -msgstr "" - -#: module/plugins/PluginManager.py:73 -msgid "Container" -msgstr "" - -#: module/plugins/PluginManager.py:74 -msgid "Hoster" -msgstr "" - -#: module/plugins/PluginManager.py:76 -msgid "Captcha" -msgstr "" - -#: module/plugins/PluginManager.py:77 -msgid "Account" -msgstr "" - -#: module/plugins/PluginManager.py:78 -msgid "Hook" -msgstr "" - -#: module/plugins/PluginManager.py:159 +#: module/plugins/PluginManager.py:153  #, python-format  msgid "%s has a invalid pattern."  msgstr "" -#: module/plugins/PluginManager.py:320 +#: module/plugins/PluginManager.py:272  #, python-format  msgid "Error importing %(name)s: %(msg)s"  msgstr "" -#: module/plugins/PluginManager.py:321 -msgid "You should fix dependicies or deactivate it." +#: module/plugins/AccountManager.py:88 +msgid "Account settings deleted, due to new config format."  msgstr "" -#: module/plugins/AccountManager.py:87 -msgid "Account settings deleted, due to new config format." +#: module/plugins/internal/MultiHoster.py:60 +msgid "No Hoster loaded"  msgstr ""  #: module/plugins/Account.py:85 module/plugins/Account.py:91 @@ -809,73 +789,78 @@ msgstr ""  msgid "Converting old Django DB"  msgstr "" -#: module/PluginThread.py:182 +#: module/network/HTTPDownload.py:245 +#, python-format +msgid "Download chunks failed, fallback to single connection | %s" +msgstr "" + +#: module/PluginThread.py:183  #, python-format  msgid "Download starts: %s"  msgstr "" -#: module/PluginThread.py:189 module/PluginThread.py:364 +#: module/PluginThread.py:189 +#, python-format +msgid "Download finished: %s" +msgstr "" + +#: module/PluginThread.py:194 module/PluginThread.py:366  #, python-format  msgid "Plugin %s is missing a function."  msgstr "" -#: module/PluginThread.py:197 module/PluginThread.py:260 -#: module/PluginThread.py:381 +#: module/PluginThread.py:202 module/PluginThread.py:265 +#: module/PluginThread.py:383  #, python-format  msgid "Download aborted: %s"  msgstr "" -#: module/PluginThread.py:217 +#: module/PluginThread.py:222  #, python-format  msgid "Download restarted: %(name)s | %(msg)s"  msgstr "" -#: module/PluginThread.py:226 module/PluginThread.py:372 +#: module/PluginThread.py:231 module/PluginThread.py:374  #, python-format  msgid "Download is offline: %s"  msgstr "" -#: module/PluginThread.py:229 +#: module/PluginThread.py:234  #, python-format  msgid "Download is temporary offline: %s"  msgstr "" -#: module/PluginThread.py:232 module/PluginThread.py:299 +#: module/PluginThread.py:237 module/PluginThread.py:304  #, python-format  msgid "Download failed: %(name)s | %(msg)s"  msgstr "" -#: module/PluginThread.py:249 +#: module/PluginThread.py:254  msgid ""  "Couldn't connect to host or connection reset, waiting 1 minute and retry."  msgstr "" -#: module/PluginThread.py:285 +#: module/PluginThread.py:290  #, python-format  msgid "Download skipped: %(name)s due to %(plugin)s"  msgstr "" -#: module/PluginThread.py:315 -#, python-format -msgid "Download finished: %s" -msgstr "" - -#: module/PluginThread.py:360 +#: module/PluginThread.py:362  #, python-format  msgid "Decrypting starts: %s"  msgstr "" -#: module/PluginThread.py:375 module/PluginThread.py:393 +#: module/PluginThread.py:377 module/PluginThread.py:395  #, python-format  msgid "Decrypting failed: %(name)s | %(msg)s"  msgstr "" -#: module/PluginThread.py:387 +#: module/PluginThread.py:389  #, python-format  msgid "Retrying %s"  msgstr "" -#: module/PluginThread.py:608 +#: module/PluginThread.py:636  #, python-format  msgid "Info Fetching for %(name)s failed | %(err)s"  msgstr "" diff --git a/locale/django.pot b/locale/django.pot index 999a00adc..81c9c7b6b 100644 --- a/locale/django.pot +++ b/locale/django.pot @@ -8,7 +8,7 @@ msgid ""  msgstr ""  "Project-Id-Version: pyLoad 0.4.9\n"  "Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" -"POT-Creation-Date: 2011-10-11 18:59+0200\n" +"POT-Creation-Date: 2011-12-07 19:21+0100\n"  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"  "Language-Team: LANGUAGE <LL@li.org>\n" @@ -42,47 +42,63 @@ msgid "Success"  msgstr ""  #: module/web/translations.js:6 -#: module/web/templates/default/filemanager.html:65 -#: module/web/templates/default/folder.html:14 -msgid "Folder is empty" +msgid "Passwords did not match."  msgstr ""  #: module/web/translations.js:7 -msgid "Restart Link" +msgid "Delete Link"  msgstr ""  #: module/web/translations.js:8 -msgid "Delete Link" +msgid "pyLoad restarted"  msgstr ""  #: module/web/translations.js:9 -msgid "Please Enter a packagename." +msgid "You are really sure you want to quit pyLoad?"  msgstr ""  #: module/web/translations.js:10 -msgid "Please click on the right captcha position." +msgid "Please Enter a packagename."  msgstr ""  #: module/web/translations.js:11 -msgid "Error occured." +msgid "Please click on the right captcha position."  msgstr ""  #: module/web/translations.js:12 -msgid "New Captcha Request" +msgid "Error occured."  msgstr ""  #: module/web/translations.js:13 -msgid "Failed" +msgid "New Captcha Request"  msgstr ""  #: module/web/translations.js:14 -msgid "No Captchas to read." +msgid "Failed"  msgstr ""  #: module/web/translations.js:15 +msgid "No Captchas to read." +msgstr "" + +#: module/web/translations.js:16 +#: module/web/templates/default/filemanager.html:65 +#: module/web/templates/default/folder.html:14 +msgid "Folder is empty" +msgstr "" + +#: module/web/translations.js:17 +msgid "Restart Link" +msgstr "" + +#: module/web/translations.js:18  msgid "New folder"  msgstr "" +#: module/web/translations.js:19 +msgid "Are you sure you want to restart pyLoad?" +msgstr "" +  #: module/web/pyload_app.py:125  msgid "You dont have permission to access this page."  msgstr "" @@ -176,93 +192,102 @@ msgstr ""  msgid "FileManager"  msgstr "" -#: module/web/templates/default/admin.html:58 -#: module/web/templates/default/admin.html:59 -msgid "Administrate User" +#: module/web/templates/default/admin.html:8 +#: module/web/templates/default/admin.html:9 +#: module/web/templates/default/base.html:59 +msgid "Administrate" +msgstr "" + +#: module/web/templates/default/admin.html:13 +msgid "Quit pyLoad" +msgstr "" + +#: module/web/templates/default/admin.html:14 +msgid "Restart pyLoad"  msgstr "" -#: module/web/templates/default/admin.html:63 +#: module/web/templates/default/admin.html:18  msgid "To add user or change passwords use:"  msgstr "" -#: module/web/templates/default/admin.html:64 +#: module/web/templates/default/admin.html:19  msgid "Important: Admin user have always all permissions!"  msgstr "" -#: module/web/templates/default/admin.html:70 +#: module/web/templates/default/admin.html:25  #: module/web/templates/default/settings.html:91 -#: module/web/templates/default/queue.html:80 +#: module/web/templates/default/queue.html:82  #: module/web/templates/default/window.html:7  #: module/web/templates/default/home.html:237  msgid "Name"  msgstr "" -#: module/web/templates/default/admin.html:73 -#: module/web/templates/default/admin.html:112 +#: module/web/templates/default/admin.html:28 +#: module/web/templates/default/admin.html:67  msgid "Change Password"  msgstr "" -#: module/web/templates/default/admin.html:76 +#: module/web/templates/default/admin.html:31  msgid "Admin"  msgstr "" -#: module/web/templates/default/admin.html:79 +#: module/web/templates/default/admin.html:34  msgid "Permissions"  msgstr "" -#: module/web/templates/default/admin.html:86 +#: module/web/templates/default/admin.html:41  msgid "change"  msgstr "" -#: module/web/templates/default/admin.html:106 -#: module/web/templates/default/admin.html:136 +#: module/web/templates/default/admin.html:61 +#: module/web/templates/default/admin.html:91  #: module/web/templates/default/settings.html:167 -#: module/web/templates/default/queue.html:95 +#: module/web/templates/default/queue.html:97  #: module/web/templates/default/captcha.html:33  msgid "Submit"  msgstr "" -#: module/web/templates/default/admin.html:114 +#: module/web/templates/default/admin.html:69  msgid "Enter your current and desired Password."  msgstr "" -#: module/web/templates/default/admin.html:115 +#: module/web/templates/default/admin.html:70  msgid "User"  msgstr "" -#: module/web/templates/default/admin.html:116 +#: module/web/templates/default/admin.html:71  #: module/web/templates/default/settings.html:179  msgid "Your username."  msgstr "" -#: module/web/templates/default/admin.html:120 +#: module/web/templates/default/admin.html:75  msgid "Current password"  msgstr "" -#: module/web/templates/default/admin.html:121 +#: module/web/templates/default/admin.html:76  #: module/web/templates/default/settings.html:184  msgid "The password for this account."  msgstr "" -#: module/web/templates/default/admin.html:125 +#: module/web/templates/default/admin.html:80  msgid "New password"  msgstr "" -#: module/web/templates/default/admin.html:126 +#: module/web/templates/default/admin.html:81  msgid "The new password."  msgstr "" -#: module/web/templates/default/admin.html:130 +#: module/web/templates/default/admin.html:85  msgid "New password (repeat)"  msgstr "" -#: module/web/templates/default/admin.html:131 +#: module/web/templates/default/admin.html:86  msgid "Please repeat the new password."  msgstr "" -#: module/web/templates/default/admin.html:137 +#: module/web/templates/default/admin.html:92  #: module/web/templates/default/settings.html:198 -#: module/web/templates/default/queue.html:96 +#: module/web/templates/default/queue.html:98  #: module/web/templates/default/window.html:41  msgid "Reset"  msgstr "" @@ -298,7 +323,7 @@ msgstr ""  #: module/web/templates/default/settings.html:92  #: module/web/templates/default/settings.html:183  #: module/web/templates/default/login.html:19 -#: module/web/templates/default/queue.html:90 +#: module/web/templates/default/queue.html:92  #: module/web/templates/default/window.html:21  msgid "Password"  msgstr "" @@ -452,10 +477,6 @@ msgstr ""  msgid "Logout"  msgstr "" -#: module/web/templates/default/base.html:59 -msgid "Administrate" -msgstr "" -  #: module/web/templates/default/base.html:61  msgid "Info"  msgstr "" @@ -555,35 +576,35 @@ msgstr ""  msgid "Restart Failed"  msgstr "" -#: module/web/templates/default/queue.html:64 +#: module/web/templates/default/queue.html:65  msgid "Folder:"  msgstr "" -#: module/web/templates/default/queue.html:64 +#: module/web/templates/default/queue.html:65  msgid "Password:"  msgstr "" -#: module/web/templates/default/queue.html:77 +#: module/web/templates/default/queue.html:79  msgid "Edit Package"  msgstr "" -#: module/web/templates/default/queue.html:78 +#: module/web/templates/default/queue.html:80  msgid "Edit the package detais below."  msgstr "" -#: module/web/templates/default/queue.html:81 +#: module/web/templates/default/queue.html:83  msgid "The name of the package."  msgstr "" -#: module/web/templates/default/queue.html:85 +#: module/web/templates/default/queue.html:87  msgid "Folder"  msgstr "" -#: module/web/templates/default/queue.html:86 +#: module/web/templates/default/queue.html:88  msgid "Name of subfolder for these downloads."  msgstr "" -#: module/web/templates/default/queue.html:91 +#: module/web/templates/default/queue.html:93  msgid "List of passwords used for unrar."  msgstr "" diff --git a/locale/gui.pot b/locale/gui.pot index 46543363b..dc74397a0 100644 --- a/locale/gui.pot +++ b/locale/gui.pot @@ -8,7 +8,7 @@ msgid ""  msgstr ""  "Project-Id-Version: pyLoad 0.4.9\n"  "Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" -"POT-Creation-Date: 2011-10-11 18:59+0200\n" +"POT-Creation-Date: 2011-12-07 19:21+0100\n"  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"  "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,46 +17,46 @@ msgstr ""  "Content-Type: text/plain; charset=UTF-8\n"  "Content-Transfer-Encoding: 8bit\n" -#: pyLoadGui.py:288 +#: pyLoadGui.py:290  msgid "paused"  msgstr "" -#: pyLoadGui.py:290 +#: pyLoadGui.py:292  msgid "running"  msgstr "" -#: pyLoadGui.py:330 +#: pyLoadGui.py:332  msgid "Unnamed"  msgstr "" -#: pyLoadGui.py:653 +#: pyLoadGui.py:655  #, python-format  msgid "Finished downloading of '%s'"  msgstr "" -#: pyLoadGui.py:655 +#: pyLoadGui.py:657  #, python-format  msgid "Failed downloading '%s'!"  msgstr "" -#: pyLoadGui.py:658 +#: pyLoadGui.py:660  #, python-format  msgid "Added '%s' to queue"  msgstr "" -#: pyLoadGui.py:683 +#: pyLoadGui.py:685  msgid "Connection lost"  msgstr "" -#: pyLoadGui.py:683 +#: pyLoadGui.py:685  msgid "Lost connection to the core!"  msgstr "" -#: pyLoadGui.py:718 +#: pyLoadGui.py:720  msgid "Show"  msgstr "" -#: pyLoadGui.py:723 module/gui/MainWindow.py:133 +#: pyLoadGui.py:725 module/gui/MainWindow.py:133  msgid "Exit"  msgstr "" @@ -146,7 +146,7 @@ msgstr ""  msgid "Status"  msgstr "" -#: module/gui/Collector.py:290 module/gui/Queue.py:160 +#: module/gui/Collector.py:290 module/gui/Queue.py:158  msgid "Size"  msgstr "" @@ -468,15 +468,15 @@ msgstr ""  msgid "Create account"  msgstr "" -#: module/gui/Queue.py:162 +#: module/gui/Queue.py:160  msgid "ETA"  msgstr "" -#: module/gui/Queue.py:164 +#: module/gui/Queue.py:162  msgid "Progress"  msgstr "" -#: module/gui/Queue.py:393 +#: module/gui/Queue.py:384  #, python-format  msgid "waiting %d seconds"  msgstr "" diff --git a/locale/setup.pot b/locale/setup.pot index a593f002c..cf4dd8cfc 100644 --- a/locale/setup.pot +++ b/locale/setup.pot @@ -8,7 +8,7 @@ msgid ""  msgstr ""  "Project-Id-Version: pyLoad 0.4.9\n"  "Report-Msgid-Bugs-To: 'bugs@pyload.org'\n" -"POT-Creation-Date: 2011-10-11 18:59+0200\n" +"POT-Creation-Date: 2011-12-07 19:21+0100\n"  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"  "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,423 +17,479 @@ msgstr ""  "Content-Type: text/plain; charset=UTF-8\n"  "Content-Transfer-Encoding: 8bit\n" -#: module/setup.py:67 +#: module/setup.py:51 +msgid "y" +msgstr "" + +#: module/setup.py:53 +msgid "n" +msgstr "" + +#: module/setup.py:72  msgid "Welcome to the pyLoad Configuration Assistent."  msgstr "" -#: module/setup.py:68 +#: module/setup.py:73  msgid ""  "It will check your system and make a basic setup in order to run pyLoad."  msgstr "" -#: module/setup.py:70 +#: module/setup.py:75  msgid "The value in brackets [] always is the default value,"  msgstr "" -#: module/setup.py:71 +#: module/setup.py:76  msgid ""  "in case you don't want to change it or you are unsure what to choose, just "  "hit enter."  msgstr "" -#: module/setup.py:72 +#: module/setup.py:77  msgid ""  "Don't forget: You can always rerun this assistent with --setup or -s "  "parameter, when you start pyLoadCore."  msgstr "" -#: module/setup.py:73 +#: module/setup.py:78  msgid "If you have any problems with this assistent hit STRG-C,"  msgstr "" -#: module/setup.py:74 +#: module/setup.py:79  msgid "to abort and don't let him start with pyLoadCore automatically anymore."  msgstr "" -#: module/setup.py:76 +#: module/setup.py:81  msgid "When you are ready for system check, hit enter."  msgstr "" -#: module/setup.py:83 +#: module/setup.py:88  msgid "You need pycurl, sqlite and python 2.5, 2.6 or 2.7 to run pyLoad."  msgstr "" -#: module/setup.py:84 +#: module/setup.py:89  msgid "Please correct this and re-run pyLoad."  msgstr "" -#: module/setup.py:85 +#: module/setup.py:90  msgid "Setup will now close."  msgstr "" -#: module/setup.py:89 +#: module/setup.py:94  msgid "System check finished, hit enter to see your status report."  msgstr "" -#: module/setup.py:91 +#: module/setup.py:96  msgid "## Status ##"  msgstr "" -#: module/setup.py:96 +#: module/setup.py:101  msgid "container decrypting"  msgstr "" -#: module/setup.py:97 +#: module/setup.py:102  msgid "ssl connection"  msgstr "" -#: module/setup.py:98 +#: module/setup.py:103  msgid "automatic captcha decryption"  msgstr "" -#: module/setup.py:99 +#: module/setup.py:104  msgid "GUI"  msgstr "" -#: module/setup.py:100 +#: module/setup.py:105  msgid "Webinterface"  msgstr "" -#: module/setup.py:101 +#: module/setup.py:106  msgid "extended Click'N'Load"  msgstr "" -#: module/setup.py:108 +#: module/setup.py:113  msgid "Features available:"  msgstr "" -#: module/setup.py:112 +#: module/setup.py:117  msgid "Featues missing: "  msgstr "" -#: module/setup.py:116 +#: module/setup.py:121  msgid "no py-crypto available"  msgstr "" -#: module/setup.py:117 +#: module/setup.py:122  msgid "You need this if you want to decrypt container files."  msgstr "" -#: module/setup.py:121 +#: module/setup.py:126  msgid "no SSL available"  msgstr "" -#: module/setup.py:122 +#: module/setup.py:127  msgid ""  "This is needed if you want to establish a secure connection to core or "  "webinterface."  msgstr "" -#: module/setup.py:123 +#: module/setup.py:128  msgid "If you only want to access locally to pyLoad ssl is not usefull."  msgstr "" -#: module/setup.py:127 +#: module/setup.py:132  msgid "no Captcha Recognition available"  msgstr "" -#: module/setup.py:128 +#: module/setup.py:133  msgid "Only needed for some hosters and as freeuser."  msgstr "" -#: module/setup.py:132 +#: module/setup.py:137  msgid "Gui not available"  msgstr "" -#: module/setup.py:133 +#: module/setup.py:138  msgid "The Graphical User Interface."  msgstr "" -#: module/setup.py:137 +#: module/setup.py:142  msgid "no JavaScript engine found"  msgstr "" -#: module/setup.py:138 +#: module/setup.py:143  msgid ""  "You will need this for some Click'N'Load links. Install Spidermonkey, ossp-"  "js, pyv8 or rhino"  msgstr "" -#: module/setup.py:140 +#: module/setup.py:145  msgid "You can abort the setup now and fix some dependicies if you want."  msgstr "" -#: module/setup.py:142 +#: module/setup.py:147  msgid "Continue with setup?"  msgstr "" -#: module/setup.py:148 +#: module/setup.py:153  #, python-format  msgid "Do you want to change the config path? Current is %s"  msgstr "" -#: module/setup.py:149 +#: module/setup.py:154  msgid ""  "If you use pyLoad on a server or the home partition lives on an iternal "  "flash it may be a good idea to change it."  msgstr "" -#: module/setup.py:150 +#: module/setup.py:155  msgid "Change config path?"  msgstr "" -#: module/setup.py:157 +#: module/setup.py:162  msgid "Do you want to configure login data and basic settings?"  msgstr "" -#: module/setup.py:158 +#: module/setup.py:163  msgid "This is recommend for first run."  msgstr "" -#: module/setup.py:159 +#: module/setup.py:164  msgid "Make basic setup?"  msgstr "" -#: module/setup.py:166 +#: module/setup.py:171  msgid "Do you want to configure ssl?"  msgstr "" -#: module/setup.py:167 +#: module/setup.py:172  msgid "Configure ssl?"  msgstr "" -#: module/setup.py:173 +#: module/setup.py:178  msgid "Do you want to configure webinterface?"  msgstr "" -#: module/setup.py:174 +#: module/setup.py:179  msgid "Configure webinterface?"  msgstr "" -#: module/setup.py:179 +#: module/setup.py:184  msgid "Setup finished successfully."  msgstr "" -#: module/setup.py:180 +#: module/setup.py:185  msgid "Hit enter to exit and restart pyLoad"  msgstr "" -#: module/setup.py:186 +#: module/setup.py:191  msgid "## System Check ##"  msgstr "" -#: module/setup.py:191 +#: module/setup.py:196  msgid "Your python version is to new, Please use Python 2.6/2.7"  msgstr "" -#: module/setup.py:194 +#: module/setup.py:199  msgid "Your python version is to old, Please use at least Python 2.5"  msgstr "" -#: module/setup.py:197 +#: module/setup.py:202  msgid "Python Version: OK"  msgstr "" -#: module/setup.py:242 +#: module/setup.py:249  #, python-format  msgid "Your installed jinja2 version %s seems too old."  msgstr "" -#: module/setup.py:243 +#: module/setup.py:250  msgid "You can safely continue but if the webinterface is not working,"  msgstr "" -#: module/setup.py:244 +#: module/setup.py:251  msgid ""  "please upgrade or deinstall it, pyLoad includes a sufficient jinja2 libary."  msgstr "" -#: module/setup.py:261 +#: module/setup.py:268  msgid "JS engine"  msgstr "" -#: module/setup.py:267 +#: module/setup.py:274  msgid "## Basic Setup ##"  msgstr "" -#: module/setup.py:270 +#: module/setup.py:277  msgid "The following logindata is valid for CLI, GUI and webinterface."  msgstr "" -#: module/setup.py:275 module/setup.py:358 module/setup.py:374 +#: module/setup.py:282 module/setup.py:371 module/setup.py:387  msgid "Username"  msgstr "" -#: module/setup.py:282 +#: module/setup.py:288 +msgid "" +"External clients (GUI, CLI or other) need remote access to work over the " +"network." +msgstr "" + +#: module/setup.py:289 +msgid "" +"However, if you only want to use the webinterface you may disable it to save " +"ram." +msgstr "" + +#: module/setup.py:290 +msgid "Enable remote access" +msgstr "" + +#: module/setup.py:295  msgid "Language"  msgstr "" -#: module/setup.py:285 +#: module/setup.py:298  msgid "Downloadfolder"  msgstr "" -#: module/setup.py:286 +#: module/setup.py:299  msgid "Max parallel downloads"  msgstr "" -#: module/setup.py:290 +#: module/setup.py:303  msgid "Use Reconnect?"  msgstr "" -#: module/setup.py:293 +#: module/setup.py:306  msgid "Reconnect script location"  msgstr "" -#: module/setup.py:298 +#: module/setup.py:311  msgid "## Webinterface Setup ##"  msgstr "" -#: module/setup.py:301 +#: module/setup.py:314  msgid "Activate webinterface?"  msgstr "" -#: module/setup.py:303 +#: module/setup.py:316  msgid ""  "Listen address, if you use 127.0.0.1 or localhost, the webinterface will "  "only accessible locally."  msgstr "" -#: module/setup.py:304 +#: module/setup.py:317  msgid "Address"  msgstr "" -#: module/setup.py:305 +#: module/setup.py:318  msgid "Port"  msgstr "" -#: module/setup.py:307 +#: module/setup.py:320  msgid ""  "pyLoad offers several server backends, now following a short explanation."  msgstr "" -#: module/setup.py:308 +#: module/setup.py:321  msgid "Default server, best choice if you dont know which one to choose."  msgstr "" -#: module/setup.py:309 +#: module/setup.py:322  msgid "This server offers SSL and is a good alternative to builtin."  msgstr "" -#: module/setup.py:310 +#: module/setup.py:323  msgid ""  "Can be used by apache, lighttpd, requires you to configure them, which is "  "not too easy job."  msgstr "" -#: module/setup.py:311 +#: module/setup.py:324  msgid "Very fast alternative written in C, requires libev and linux knowlegde."  msgstr "" -#: module/setup.py:312 +#: module/setup.py:325  msgid "Get it from here: https://github.com/jonashaag/bjoern, compile it"  msgstr "" -#: module/setup.py:313 +#: module/setup.py:326  msgid "and copy bjoern.so to module/lib"  msgstr "" -#: module/setup.py:316 +#: module/setup.py:329  msgid ""  "Attention: In some rare cases the builtin server is not working, if you "  "notice problems with the webinterface"  msgstr "" -#: module/setup.py:317 +#: module/setup.py:330  msgid "come back here and change the builtin server to the threaded one here."  msgstr "" -#: module/setup.py:320 +#: module/setup.py:333  msgid "Server"  msgstr "" -#: module/setup.py:324 +#: module/setup.py:337  msgid "## SSL Setup ##"  msgstr "" -#: module/setup.py:326 +#: module/setup.py:339  msgid ""  "Execute these commands from pyLoad config folder to make ssl certificates:"  msgstr "" -#: module/setup.py:332 +#: module/setup.py:345  msgid "If you're done and everything went fine, you can activate ssl now."  msgstr "" -#: module/setup.py:334 +#: module/setup.py:347  msgid "Activate SSL?"  msgstr "" -#: module/setup.py:348 +#: module/setup.py:361  msgid "Select action"  msgstr "" -#: module/setup.py:349 +#: module/setup.py:362  msgid "1 - Create/Edit user"  msgstr "" -#: module/setup.py:350 +#: module/setup.py:363  msgid "2 - List users"  msgstr "" -#: module/setup.py:351 +#: module/setup.py:364  msgid "3 - Remove user"  msgstr "" -#: module/setup.py:352 +#: module/setup.py:365  msgid "4 - Quit"  msgstr "" -#: module/setup.py:364 +#: module/setup.py:377  msgid "Users"  msgstr "" -#: module/setup.py:392 +#: module/setup.py:406  msgid "Setting new configpath, current configuration will not be transfered!"  msgstr "" -#: module/setup.py:393 +#: module/setup.py:407  msgid "Configpath"  msgstr "" -#: module/setup.py:401 +#: module/setup.py:415  msgid "Configpath changed, setup will now close, please restart to go on."  msgstr "" -#: module/setup.py:402 +#: module/setup.py:416  msgid "Press Enter to exit."  msgstr "" -#: module/setup.py:406 +#: module/setup.py:420  #, python-format  msgid "Setting config path failed: %s"  msgstr "" -#: module/setup.py:411 +#: module/setup.py:425  #, python-format  msgid "%s: OK"  msgstr "" -#: module/setup.py:413 +#: module/setup.py:427  #, python-format  msgid "%s: missing"  msgstr ""  #: module/setup.py:456 +msgid "[y]/n" +msgstr "" + +#: module/setup.py:458 +msgid "y/[n]" +msgstr "" + +#: module/setup.py:470  msgid "Password: "  msgstr "" -#: module/setup.py:461 +#: module/setup.py:475  msgid "Password to short. Use at least 4 symbols."  msgstr "" -#: module/setup.py:467 +#: module/setup.py:481  msgid "Password (again): "  msgstr "" -#: module/setup.py:474 +#: module/setup.py:488  msgid "Passwords did not match."  msgstr "" -#: module/setup.py:489 module/setup.py:500 +#: module/setup.py:499 +msgid "yes" +msgstr "" + +#: module/setup.py:499 +msgid "true" +msgstr "" + +#: module/setup.py:499 +msgid "t" +msgstr "" + +#: module/setup.py:502 +msgid "no" +msgstr "" + +#: module/setup.py:502 +msgid "false" +msgstr "" + +#: module/setup.py:502 +msgid "f" +msgstr "" + +#: module/setup.py:505 module/setup.py:516  msgid "Invalid Input"  msgstr "" diff --git a/module/Api.py b/module/Api.py index fc36c9fea..f0bf5e264 100644 --- a/module/Api.py +++ b/module/Api.py @@ -65,6 +65,7 @@ class PERMS:      DOWNLOAD = 64  # can download from webinterface      SETTINGS = 128 # can access settings      ACCOUNTS = 256 # can access accounts +    LOGS = 512 # can see server logs  class ROLE:      ADMIN = 0  #admin has all permissions implicit @@ -249,7 +250,7 @@ class Api(Iface):          """Restart pyload core"""          self.core.do_restart = True -    @permission(PERMS.STATUS) +    @permission(PERMS.LOGS)      def getLog(self, offset=0):          """Returns most recent log entries. diff --git a/module/common/pylgettext.py b/module/common/pylgettext.py new file mode 100644 index 000000000..ae6d39325 --- /dev/null +++ b/module/common/pylgettext.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from gettext import * + +_searchdirs = None + +origfind = find + +def setpaths(pathlist): +    global _searchdirs +    if isinstance(pathlist, list): +        _searchdirs = pathlist +    else: +        _searchdirs = list(pathlist) + + +def addpath(path): +    global _searchdirs +    if _searchdirs is None: +        _searchdirs = list(path) +    else: +        if path not in _searchdirs: +            _searchdirs.append(path) + + +def delpath(path): +    global _searchdirs +    if _searchdirs is not None: +        if path in _searchdirs: +            _searchdirs.remove(path) + + +def clearpath(): +    global _searchdirs +    if _searchdirs is not None: +        _searchdirs = None + + +def find(domain, localedir=None, languages=None, all=False): +    if _searchdirs is None: +        return origfind(domain, localedir, languages, all) +    searches = [localedir] + _searchdirs +    results = list() +    for dir in searches: +        res = origfind(domain, dir, languages, all) +        if all is False: +            results.append(res) +        else: +            results.extend(res) +    if all is False: +        results = filter(lambda x: x is not None, results) +        if len(results) == 0: +            return None +        else: +            return results[0] +    else: +        return results + +#Is there a smarter/cleaner pythonic way for this? +translation.__globals__['find'] = find + diff --git a/module/lib/bottle.py b/module/lib/bottle.py index f449e182c..f8624bf13 100644 --- a/module/lib/bottle.py +++ b/module/lib/bottle.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python  # -*- coding: utf-8 -*-  """  Bottle is a fast and simple micro-framework for small web applications. It @@ -15,9 +16,26 @@ License: MIT (see LICENSE.txt for details)  from __future__ import with_statement  __author__ = 'Marcel Hellkamp' -__version__ = '0.9.1' +__version__ = '0.10.2'  __license__ = 'MIT' +# The gevent server adapter needs to patch some modules before they are imported +# This is why we parse the commandline parameters here but handle them later +if __name__ == '__main__': +    from optparse import OptionParser +    _cmd_parser = OptionParser(usage="usage: %prog [options] package.module:app") +    _opt = _cmd_parser.add_option +    _opt("--version", action="store_true", help="show version number.") +    _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") +    _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") +    _opt("-p", "--plugin", action="append", help="install additional plugin/s.") +    _opt("--debug", action="store_true", help="start server in debug mode.") +    _opt("--reload", action="store_true", help="auto-reload on file changes.") +    _cmd_options, _cmd_args = _cmd_parser.parse_args() +    if _cmd_options.server and _cmd_options.server.startswith('gevent'): +        import gevent.monkey; gevent.monkey.patch_all() + +import sys  import base64  import cgi  import email.utils @@ -30,7 +48,6 @@ import mimetypes  import os  import re  import subprocess -import sys  import tempfile  import thread  import threading @@ -38,10 +55,16 @@ import time  import warnings  from Cookie import SimpleCookie +from datetime import date as datedate, datetime, timedelta  from tempfile import TemporaryFile -from traceback import format_exc -from urllib import urlencode, quote as urlquote, unquote as urlunquote -from urlparse import urlunsplit, urljoin, SplitResult as UrlSplitResult +from traceback import format_exc, print_exc +from urlparse import urljoin, SplitResult as UrlSplitResult + +# Workaround for a bug in some versions of lib2to3 (fixed on CPython 2.7 and 3.2) +import urllib +urlencode = urllib.urlencode +urlquote = urllib.quote +urlunquote = urllib.unquote  try: from collections import MutableMapping as DictMixin  except ImportError: # pragma: no cover @@ -55,16 +78,25 @@ try: import cPickle as pickle  except ImportError: # pragma: no cover      import pickle -try: from json import dumps as json_dumps +try: from json import dumps as json_dumps, loads as json_lds  except ImportError: # pragma: no cover -    try: from simplejson import dumps as json_dumps +    try: from simplejson import dumps as json_dumps, loads as json_lds      except ImportError: # pragma: no cover -        try: from django.utils.simplejson import dumps as json_dumps +        try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds          except ImportError: # pragma: no cover -            json_dumps = None +            def json_dumps(data): +                raise ImportError("JSON support requires Python 2.6 or simplejson.") +            json_lds = json_dumps +py3k = sys.version_info >= (3,0,0)  NCTextIOWrapper = None -if sys.version_info >= (3,0,0): # pragma: no cover + +if sys.version_info < (2,6,0): +    msg = "Python 2.5 support may be dropped in future versions of Bottle." +    warnings.warn(msg, DeprecationWarning) + +if py3k: # pragma: no cover +    json_loads = lambda s: json_lds(touni(s))      # See Request.POST      from io import BytesIO      def touni(x, enc='utf8', err='strict'): @@ -77,6 +109,7 @@ if sys.version_info >= (3,0,0): # pragma: no cover                  the wrapped buffer. This subclass keeps it open. '''              def close(self): pass  else: +    json_loads = json_lds      from StringIO import StringIO as BytesIO      bytes = str      def touni(x, enc='utf8', err='strict'): @@ -87,17 +120,16 @@ def tob(data, enc='utf8'):      """ Convert anything to bytes """      return data.encode(enc) if isinstance(data, unicode) else bytes(data) -# Convert strings and unicode to native strings -if sys.version_info >= (3,0,0): -    tonat = touni -else: -    tonat = tob +tonat = touni if py3k else tob  tonat.__doc__ = """ Convert anything to native strings """ +def try_update_wrapper(wrapper, wrapped, *a, **ka): +    try: # Bug: functools breaks if wrapper is an instane method +        functools.update_wrapper(wrapper, wrapped, *a, **ka) +    except AttributeError: pass  # Backward compatibility -def depr(message, critical=False): -    if critical: raise DeprecationWarning(message) +def depr(message):      warnings.warn(message, DeprecationWarning, stacklevel=3) @@ -119,7 +151,7 @@ class DictProperty(object):          return self      def __get__(self, obj, cls): -        if not obj: return self +        if obj is None: return self          key, storage = self.key, getattr(obj, self.attr)          if key not in storage: storage[key] = self.getter(obj)          return storage[key] @@ -132,10 +164,22 @@ class DictProperty(object):          if self.read_only: raise AttributeError("Read-Only property.")          del getattr(obj, self.attr)[self.key] -def cached_property(func): -    ''' A property that, if accessed, replaces itself with the computed -        value. Subsequent accesses won't call the getter again. ''' -    return DictProperty('__dict__')(func) + +class CachedProperty(object): +    ''' A property that is only computed once per instance and then replaces +        itself with an ordinary attribute. Deleting the attribute resets the +        property. ''' + +    def __init__(self, func): +        self.func = func + +    def __get__(self, obj, cls): +        if obj is None: return self +        value = obj.__dict__[self.func.__name__] = self.func(obj) +        return value + +cached_property = CachedProperty +  class lazy_attribute(object): # Does not need configuration -> lower-case name      ''' A property that caches itself to the class object. ''' @@ -163,6 +207,8 @@ class BottleException(Exception):      pass +#TODO: These should subclass BaseRequest +  class HTTPResponse(BottleException):      """ Used to break execution and immediately finish the response """      def __init__(self, output='', status=200, header=None): @@ -207,15 +253,14 @@ class RouteReset(BottleException):      """ If raised by a plugin or request handler, the route is reset and all          plugins are re-applied. """ +class RouterUnknownModeError(RouteError): pass  class RouteSyntaxError(RouteError):      """ The route parser found something not supported by this router """ -  class RouteBuildError(RouteError):      """ The route could not been built """ -  class Router(object):      ''' A Router is an ordered collection of route->target pairs. It is used to          efficiently match WSGI requests against a number of routes and return @@ -224,83 +269,153 @@ class Router(object):          and a HTTP method.          The path-rule is either a static path (e.g. `/contact`) or a dynamic -        path that contains wildcards (e.g. `/wiki/:page`). By default, wildcards -        consume characters up to the next slash (`/`). To change that, you may -        add a regular expression pattern (e.g. `/wiki/:page#[a-z]+#`). - -        For performance reasons, static routes (rules without wildcards) are -        checked first. Dynamic routes are searched in order. Try to avoid -        ambiguous or overlapping rules. - -        The HTTP method string matches only on equality, with two exceptions: -          * ´GET´ routes also match ´HEAD´ requests if there is no appropriate -            ´HEAD´ route installed. -          * ´ANY´ routes do match if there is no other suitable route installed. - -        An optional ``name`` parameter is used by :meth:`build` to identify -        routes. +        path that contains wildcards (e.g. `/wiki/<page>`). The wildcard syntax +        and details on the matching order are described in docs:`routing`.      ''' -    default = '[^/]+' - -    @lazy_attribute -    def syntax(cls): -        return re.compile(r'(?<!\\):([a-zA-Z_][a-zA-Z_0-9]*)?(?:#(.*?)#)?') +    default_pattern = '[^/]+' +    default_filter   = 're' +    #: Sorry for the mess. It works. Trust me. +    rule_syntax = re.compile('(\\\\*)'\ +        '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\ +          '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\ +            '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') + +    def __init__(self, strict=False): +        self.rules    = {} # A {rule: Rule} mapping +        self.builder  = {} # A rule/name->build_info mapping +        self.static   = {} # Cache for static routes: {path: {method: target}} +        self.dynamic  = [] # Cache for dynamic routes. See _compile() +        #: If true, static routes are no longer checked first. +        self.strict_order = strict +        self.filters = {'re': self.re_filter, 'int': self.int_filter, +                        'float': self.re_filter, 'path': self.path_filter} + +    def re_filter(self, conf): +        return conf or self.default_pattern, None, None + +    def int_filter(self, conf): +        return r'-?\d+', int, lambda x: str(int(x)) + +    def float_filter(self, conf): +        return r'-?\d*\.\d+', float, lambda x: str(float(x)) + +    def path_filter(self, conf): +        return r'.*?', None, None +     +    def add_filter(self, name, func): +        ''' Add a filter. The provided function is called with the configuration +        string as parameter and must return a (regexp, to_python, to_url) tuple. +        The first element is a string, the last two are callables or None. ''' +        self.filters[name] = func +     +    def parse_rule(self, rule): +        ''' Parses a rule into a (name, filter, conf) token stream. If mode is +            None, name contains a static rule part. ''' +        offset, prefix = 0, '' +        for match in self.rule_syntax.finditer(rule): +            prefix += rule[offset:match.start()] +            g = match.groups() +            if len(g[0])%2: # Escaped wildcard +                prefix += match.group(0)[len(g[0]):] +                offset = match.end() +                continue +            if prefix: yield prefix, None, None +            name, filtr, conf = g[1:4] if not g[2] is None else g[4:7] +            if not filtr: filtr = self.default_filter +            yield name, filtr, conf or None +            offset, prefix = match.end(), '' +        if offset <= len(rule) or prefix: +            yield prefix+rule[offset:], None, None + +    def add(self, rule, method, target, name=None): +        ''' Add a new route or replace the target for an existing route. ''' +        if rule in self.rules: +            self.rules[rule][method] = target +            if name: self.builder[name] = self.builder[rule] +            return + +        target = self.rules[rule] = {method: target} + +        # Build pattern and other structures for dynamic routes +        anons = 0      # Number of anonymous wildcards +        pattern = ''   # Regular expression  pattern +        filters = []   # Lists of wildcard input filters +        builder = []   # Data structure for the URL builder +        is_static = True +        for key, mode, conf in self.parse_rule(rule): +            if mode: +                is_static = False +                mask, in_filter, out_filter = self.filters[mode](conf) +                if key: +                    pattern += '(?P<%s>%s)' % (key, mask) +                else: +                    pattern += '(?:%s)' % mask +                    key = 'anon%d' % anons; anons += 1 +                if in_filter: filters.append((key, in_filter)) +                builder.append((key, out_filter or str)) +            elif key: +                pattern += re.escape(key) +                builder.append((None, key)) +        self.builder[rule] = builder +        if name: self.builder[name] = builder + +        if is_static and not self.strict_order: +            self.static[self.build(rule)] = target +            return -    def __init__(self): -        self.routes = {}  # A {rule: {method: target}} mapping -        self.rules  = []  # An ordered list of rules -        self.named  = {}  # A name->(rule, build_info) mapping -        self.static = {}  # Cache for static routes: {path: {method: target}} -        self.dynamic = [] # Cache for dynamic routes. See _compile() +        def fpat_sub(m): +            return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:' +        flat_pattern = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, pattern) -    def add(self, rule, method, target, name=None, static=False): -        ''' Add a new route or replace the target for an existing route. ''' -        if static: -            depr("Use a backslash to escape ':' in routes.") # 0.9 -            rule = rule.replace(':','\\:') +        try: +            re_match = re.compile('^(%s)$' % pattern).match +        except re.error, e: +            raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e)) + +        def match(path): +            """ Return an url-argument dictionary. """ +            url_args = re_match(path).groupdict() +            for name, wildcard_filter in filters: +                try: +                    url_args[name] = wildcard_filter(url_args[name]) +                except ValueError: +                    raise HTTPError(400, 'Path has wrong format.') +            return url_args -        if rule in self.routes: -            self.routes[rule][method.upper()] = target -        else: -            self.routes[rule] = {method.upper(): target} -            self.rules.append(rule) -            if self.static or self.dynamic: # Clear precompiler cache. -                self.static, self.dynamic = {}, {} -        if name: -            self.named[name] = (rule, None) - -    def build(self, _name, *anon, **args): -        ''' Return a string that matches a named route. Use keyword arguments -            to fill out named wildcards. Remaining arguments are appended as a -            query string. Raises RouteBuildError or KeyError.''' -        if _name not in self.named: -            raise RouteBuildError("No route with that name.", _name) -        rule, pairs = self.named[_name] -        if not pairs: -            token = self.syntax.split(rule) -            parts = [p.replace('\\:',':') for p in token[::3]] -            names = token[1::3] -            if len(parts) > len(names): names.append(None) -            pairs = zip(parts, names) -            self.named[_name] = (rule, pairs)          try: -            anon = list(anon) -            url = [s if k is None -                   else s+str(args.pop(k)) if k else s+str(anon.pop()) -                   for s, k in pairs] -        except IndexError: -            msg = "Not enough arguments to fill out anonymous wildcards." -            raise RouteBuildError(msg) +            combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, flat_pattern) +            self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1]) +            self.dynamic[-1][1].append((match, target)) +        except (AssertionError, IndexError), e: # AssertionError: Too many groups +            self.dynamic.append((re.compile('(^%s$)' % flat_pattern), +                                [(match, target)])) +        return match + +    def build(self, _name, *anons, **query): +        ''' Build an URL by filling the wildcards in a rule. ''' +        builder = self.builder.get(_name) +        if not builder: raise RouteBuildError("No route with that name.", _name) +        try: +            for i, value in enumerate(anons): query['anon%d'%i] = value +            url = ''.join([f(query.pop(n)) if n else f for (n,f) in builder]) +            return url if not query else url+'?'+urlencode(query)          except KeyError, e: -            raise RouteBuildError(*e.args) - -        if args: url += ['?', urlencode(args)] -        return ''.join(url) +            raise RouteBuildError('Missing URL argument: %r' % e.args[0])      def match(self, environ): -        ''' Return a (target, url_agrs) tuple or raise HTTPError(404/405). ''' -        targets, urlargs = self._match_path(environ) +        ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). ''' +        path, targets, urlargs = environ['PATH_INFO'] or '/', None, {} +        if path in self.static: +            targets = self.static[path] +        else: +            for combined, rules in self.dynamic: +                match = combined.match(path) +                if not match: continue +                getargs, targets = rules[match.lastindex - 1] +                urlargs = getargs(path) if getargs else {} +                break +          if not targets:              raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO']))          method = environ['REQUEST_METHOD'].upper() @@ -316,66 +431,90 @@ class Router(object):          raise HTTPError(405, "Method not allowed.",                          header=[('Allow',",".join(allowed))]) -    def _match_path(self, environ): -        ''' Optimized PATH_INFO matcher. ''' -        path = environ['PATH_INFO'] or '/' -        # Assume we are in a warm state. Search compiled rules first. -        match = self.static.get(path) -        if match: return match, {} -        for combined, rules in self.dynamic: -            match = combined.match(path) -            if not match: continue -            gpat, match = rules[match.lastindex - 1] -            return match, gpat.match(path).groupdict() if gpat else {} -        # Lazy-check if we are really in a warm state. If yes, stop here. -        if self.static or self.dynamic or not self.routes: return None, {} -        # Cold state: We have not compiled any rules yet. Do so and try again. -        if not environ.get('wsgi.run_once'): -            self._compile() -            return self._match_path(environ) -        # For run_once (CGI) environments, don't compile. Just check one by one. -        epath = path.replace(':','\\:') # Turn path into its own static rule. -        match = self.routes.get(epath) # This returns static rule only. -        if match: return match, {} -        for rule in self.rules: -            #: Skip static routes to reduce re.compile() calls. -            if rule.count(':') < rule.count('\\:'): continue -            match = self._compile_pattern(rule).match(path) -            if match: return self.routes[rule], match.groupdict() -        return None, {} - -    def _compile(self): -        ''' Prepare static and dynamic search structures. ''' -        self.static = {} -        self.dynamic = [] -        def fpat_sub(m): -            return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:' -        for rule in self.rules: -            target = self.routes[rule] -            if not self.syntax.search(rule): -                self.static[rule.replace('\\:',':')] = target -                continue -            gpat = self._compile_pattern(rule) -            fpat = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, gpat.pattern) -            gpat = gpat if gpat.groupindex else None + + +class Route(object): +    ''' This class wraps a route callback along with route specific metadata and +        configuration and applies Plugins on demand. It is also responsible for +        turing an URL path rule into a regular expression usable by the Router. +    ''' + + +    def __init__(self, app, rule, method, callback, name=None, +                 plugins=None, skiplist=None, **config): +        #: The application this route is installed to. +        self.app = app +        #: The path-rule string (e.g. ``/wiki/:page``). +        self.rule = rule +        #: The HTTP method as a string (e.g. ``GET``). +        self.method = method +        #: The original callback with no plugins applied. Useful for introspection. +        self.callback = callback +        #: The name of the route (if specified) or ``None``. +        self.name = name or None +        #: A list of route-specific plugins (see :meth:`Bottle.route`). +        self.plugins = plugins or [] +        #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). +        self.skiplist = skiplist or [] +        #: Additional keyword arguments passed to the :meth:`Bottle.route` +        #: decorator are stored in this dictionary. Used for route-specific +        #: plugin configuration and meta-data. +        self.config = ConfigDict(config) + +    def __call__(self, *a, **ka): +        depr("Some APIs changed to return Route() instances instead of"\ +             " callables. Make sure to use the Route.call method and not to"\ +             " call Route instances directly.") +        return self.call(*a, **ka) + +    @cached_property +    def call(self): +        ''' The route callback with all plugins applied. This property is +            created on demand and then cached to speed up subsequent requests.''' +        return self._make_callback() + +    def reset(self): +        ''' Forget any cached values. The next time :attr:`call` is accessed, +            all plugins are re-applied. ''' +        self.__dict__.pop('call', None) + +    def prepare(self): +        ''' Do all on-demand work immediately (useful for debugging).''' +        self.call + +    @property +    def _context(self): +        depr('Switch to Plugin API v2 and access the Route object directly.') +        return dict(rule=self.rule, method=self.method, callback=self.callback, +                    name=self.name, app=self.app, config=self.config, +                    apply=self.plugins, skip=self.skiplist) + +    def all_plugins(self): +        ''' Yield all Plugins affecting this route. ''' +        unique = set() +        for p in reversed(self.app.plugins + self.plugins): +            if True in self.skiplist: break +            name = getattr(p, 'name', False) +            if name and (name in self.skiplist or name in unique): continue +            if p in self.skiplist or type(p) in self.skiplist: continue +            if name: unique.add(name) +            yield p + +    def _make_callback(self): +        callback = self.callback +        for plugin in self.all_plugins():              try: -                combined = '%s|(%s)' % (self.dynamic[-1][0].pattern, fpat) -                self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1]) -                self.dynamic[-1][1].append((gpat, target)) -            except (AssertionError, IndexError), e: # AssertionError: Too many groups -                self.dynamic.append((re.compile('(^%s$)'%fpat), -                                    [(gpat, target)])) -            except re.error, e: -                raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e)) - -    def _compile_pattern(self, rule): -        ''' Return a regular expression with named groups for each wildcard. ''' -        out = '' -        for i, part in enumerate(self.syntax.split(rule)): -            if i%3 == 0:   out += re.escape(part.replace('\\:',':')) -            elif i%3 == 1: out += '(?P<%s>' % part if part else '(?:' -            else:          out += '%s)' % (part or '[^/]+') -        return re.compile('^%s$'%out) +                if hasattr(plugin, 'apply'): +                    api = getattr(plugin, 'api', 1) +                    context = self if api > 1 else self._context +                    callback = plugin.apply(callback, context) +                else: +                    callback = plugin(callback) +            except RouteReset: # Try again with changed configuration. +                return self._make_callback() +            if not callback is self.callback: +                try_update_wrapper(callback, self.callback) +        return callback @@ -394,61 +533,62 @@ class Bottle(object):          """ Create a new bottle instance.              You usually don't do that. Use `bottle.app.push()` instead.          """ -        self.routes = [] # List of installed routes including metadata. -        self.router = Router() # Maps requests to self.route indices. -        self.ccache = {} # Cache for callbacks with plugins applied. - +        self.routes = [] # List of installed :class:`Route` instances. +        self.router = Router() # Maps requests to :class:`Route` instances.          self.plugins = [] # List of installed plugins. -        self.mounts = {}          self.error_handler = {}          #: If true, most exceptions are catched and returned as :exc:`HTTPError` +        self.config = ConfigDict(config or {})          self.catchall = catchall -        self.config = config or {} -        self.serve = True -        # Default plugins -        self.hooks = self.install(HooksPlugin()) -        self.typefilter = self.install(TypeFilterPlugin()) +        #: An instance of :class:`HooksPlugin`. Empty by default. +        self.hooks = HooksPlugin() +        self.install(self.hooks)          if autojson:              self.install(JSONPlugin())          self.install(TemplatePlugin()) -    def optimize(self, *a, **ka): -        depr("Bottle.optimize() is obsolete.") +    def mount(self, prefix, app, **options): +        ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific +            URL prefix. Example:: -    def mount(self, app, prefix, **options): -        ''' Mount an application to a specific URL prefix. The prefix is added -            to SCIPT_PATH and removed from PATH_INFO before the sub-application -            is called. +                root_app.mount('/admin/', admin_app) -            :param app: an instance of :class:`Bottle`. -            :param prefix: path prefix used as a mount-point. +            :param prefix: path prefix or `mount-point`. If it ends in a slash, +                that slash is mandatory. +            :param app: an instance of :class:`Bottle` or a WSGI application.              All other parameters are passed to the underlying :meth:`route` call.          ''' -        if not isinstance(app, Bottle): -            raise TypeError('Only Bottle instances are supported for now.') -        prefix = '/'.join(filter(None, prefix.split('/'))) -        if not prefix: -            raise TypeError('Empty prefix. Perhaps you want a merge()?') -        for other in self.mounts: -            if other.startswith(prefix): -                raise TypeError('Conflict with existing mount: %s' % other) -        path_depth = prefix.count('/') + 1 -        options.setdefault('method', 'ANY') +        if isinstance(app, basestring): +            prefix, app = app, prefix +            depr('Parameter order of Bottle.mount() changed.') # 0.10 + +        parts = filter(None, prefix.split('/')) +        if not parts: raise ValueError('Empty path prefix.') +        path_depth = len(parts)          options.setdefault('skip', True) -        self.mounts[prefix] = app -        @self.route('/%s/:#.*#' % prefix, **options) -        def mountpoint(): -            request.path_shift(path_depth) -            return app.handle(request.environ) +        options.setdefault('method', 'ANY') -    def add_filter(self, ftype, func): -        depr("Filters are deprecated and can be replaced with plugins.") #0.9 -        self.typefilter.add(ftype, func) +        @self.route('/%s/:#.*#' % '/'.join(parts), **options) +        def mountpoint(): +            try: +                request.path_shift(path_depth) +                rs = BaseResponse([], 200) +                def start_response(status, header): +                    rs.status = status +                    for name, value in header: rs.add_header(name, value) +                    return rs.body.append +                rs.body = itertools.chain(rs.body, app(request.environ, start_response)) +                return HTTPResponse(rs.body, rs.status, rs.headers) +            finally: +                request.path_shift(-path_depth) + +        if not prefix.endswith('/'): +            self.route('/' + '/'.join(parts), callback=mountpoint, **options)      def install(self, plugin): -        ''' Add a plugin to the list of plugins and prepare it for beeing +        ''' Add a plugin to the list of plugins and prepare it for being              applied to all routes of this application. A plugin may be a simple              decorator or an object that implements the :class:`Plugin` API.          ''' @@ -460,11 +600,10 @@ class Bottle(object):          return plugin      def uninstall(self, plugin): -        ''' Uninstall plugins. Pass an instance to remove a specific plugin. -            Pass a type object to remove all plugins that match that type. -            Subclasses are not removed. Pass a string to remove all plugins with -            a matching ``name`` attribute. Pass ``True`` to remove all plugins. -            The list of affected plugins is returned. ''' +        ''' Uninstall plugins. Pass an instance to remove a specific plugin, a type +            object to remove all plugins that match that type, a string to remove +            all plugins with a matching ``name`` attribute or ``True`` to remove all +            plugins. Return the list of removed plugins. '''          removed, remove = [], plugin          for i, plugin in list(enumerate(self.plugins))[::-1]:              if remove is True or remove is plugin or remove is type(plugin) \ @@ -475,15 +614,17 @@ class Bottle(object):          if removed: self.reset()          return removed -    def reset(self, id=None): +    def reset(self, route=None):          ''' Reset all routes (force plugins to be re-applied) and clear all -            caches. If an ID is given, only that specific route is affected. ''' -        if id is None: self.ccache.clear() -        else: self.ccache.pop(id, None) +            caches. If an ID or route object is given, only that specific route +            is affected. ''' +        if route is None: routes = self.routes +        elif isinstance(route, Route): routes = [route] +        else: routes = [self.routes[route]] +        for route in routes: route.reset()          if DEBUG: -            for route in self.routes: -                if route['id'] not in self.ccache: -                    self.ccache[route['id']] = self._build_callback(route) +            for route in routes: route.prepare() +        self.hooks.trigger('app_reset')      def close(self):          ''' Close the application and all installed plugins. ''' @@ -492,45 +633,10 @@ class Bottle(object):          self.stopped = True      def match(self, environ): -        """ (deprecated) Search for a matching route and return a -            (callback, urlargs) tuple. -            The first element is the associated route callback with plugins -            applied. The second value is a dictionary with parameters extracted -            from the URL. The :class:`Router` raises :exc:`HTTPError` (404/405) -            on a non-match.""" -        depr("This method will change semantics in 0.10.") -        return self._match(environ) -         -    def _match(self, environ): -        handle, args = self.router.match(environ) -        environ['route.handle'] = handle # TODO move to router? -        environ['route.url_args'] = args -        try: -            return self.ccache[handle], args -        except KeyError: -            config = self.routes[handle] -            callback = self.ccache[handle] = self._build_callback(config) -            return callback, args - -    def _build_callback(self, config): -        ''' Apply plugins to a route and return a new callable. ''' -        wrapped = config['callback'] -        plugins = self.plugins + config['apply'] -        skip    = config['skip'] -        try: -            for plugin in reversed(plugins): -                if True in skip: break -                if plugin in skip or type(plugin) in skip: continue -                if getattr(plugin, 'name', True) in skip: continue -                if hasattr(plugin, 'apply'): -                    wrapped = plugin.apply(wrapped, config) -                else: -                    wrapped = plugin(wrapped) -                if not wrapped: break -                functools.update_wrapper(wrapped, config['callback']) -            return wrapped -        except RouteReset: # A plugin may have changed the config dict inplace. -            return self._build_callback(config) # Apply all plugins again. +        """ Search for a matching route and return a (:class:`Route` , urlargs) +            tuple. The second value is a dictionary with parameters extracted +            from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" +        return self.router.match(environ)      def get_url(self, routename, **kargs):          """ Return a string that matches a named route """ @@ -566,31 +672,20 @@ class Bottle(object):              configuration and passed to plugins (see :meth:`Plugin.apply`).          """          if callable(path): path, callback = None, path -          plugins = makelist(apply)          skiplist = makelist(skip) -        if 'decorate' in config: -            depr("The 'decorate' parameter was renamed to 'apply'") # 0.9 -            plugins += makelist(config.pop('decorate')) -        if config.pop('no_hooks', False): -            depr("The no_hooks parameter is no longer used. Add 'hooks' to the"\ -                 " list of skipped plugins instead.") # 0.9 -            skiplist.append('hooks') -        static = config.get('static', False) # depr 0.9 -          def decorator(callback): +            # TODO: Documentation and tests +            if isinstance(callback, basestring): callback = load(callback)              for rule in makelist(path) or yieldroutes(callback):                  for verb in makelist(method):                      verb = verb.upper() -                    cfg = dict(rule=rule, method=verb, callback=callback, -                               name=name, app=self, config=config, -                               apply=plugins, skip=skiplist) -                    self.routes.append(cfg) -                    cfg['id'] = self.routes.index(cfg) -                    self.router.add(rule, verb, cfg['id'], name=name, static=static) -                    if DEBUG: self.ccache[cfg['id']] = self._build_callback(cfg) +                    route = Route(self, rule, verb, callback, name=name, +                                  plugins=plugins, skiplist=skiplist, **config) +                    self.routes.append(route) +                    self.router.add(rule, verb, route, name=name) +                    if DEBUG: route.prepare()              return callback -          return decorator(callback) if callback else decorator      def get(self, path=None, method='GET', **options): @@ -623,14 +718,6 @@ class Bottle(object):              return func          return wrapper -    def add_hook(self, name, func): -        depr("Call Bottle.hooks.add() instead.") #0.9 -        self.hooks.add(name, func) - -    def remove_hook(self, name, func): -        depr("Call Bottle.hooks.remove() instead.") #0.9 -        self.hooks.remove(name, func) -      def handle(self, path, method='GET'):          """ (deprecated) Execute the first matching route callback and return              the result. :exc:`HTTPResponse` exceptions are catched and returned. @@ -641,24 +728,25 @@ class Bottle(object):          if isinstance(path, dict):              return self._handle(path)          return self._handle({'PATH_INFO': path, 'REQUEST_METHOD': method.upper()}) -         +      def _handle(self, environ): -        if not self.serve: -            depr("Bottle.serve will be removed in 0.10.") -            return HTTPError(503, "Server stopped")          try: -            callback, args = self._match(environ) -            return callback(**args) +            route, args = self.router.match(environ) +            environ['route.handle'] = environ['bottle.route'] = route +            environ['route.url_args'] = args +            return route.call(**args)          except HTTPResponse, r:              return r -        except RouteReset: # Route reset requested by the callback or a plugin. -            del self.ccache[handle] -            return self.handle(environ) # Try again. +        except RouteReset: +            route.reset() +            return self._handle(environ)          except (KeyboardInterrupt, SystemExit, MemoryError):              raise          except Exception, e:              if not self.catchall: raise -            return HTTPError(500, "Internal Server Error", e, format_exc(10)) +            stacktrace = format_exc(10) +            environ['wsgi.errors'].write(stacktrace) +            return HTTPError(500, "Internal Server Error", e, stacktrace)      def _cast(self, out, request, response, peek=None):          """ Try to convert the parameter into something WSGI compatible and set @@ -669,7 +757,7 @@ class Bottle(object):          # Empty output is done here          if not out: -            response.headers['Content-Length'] = 0 +            response['Content-Length'] = 0              return []          # Join lists of byte or unicode strings. Mixed lists are NOT supported          if isinstance(out, (tuple, list))\ @@ -680,9 +768,10 @@ class Bottle(object):              out = out.encode(response.charset)          # Byte Strings are just returned          if isinstance(out, bytes): -            response.headers['Content-Length'] = str(len(out)) +            response['Content-Length'] = len(out)              return [out]          # HTTPError or HTTPException (recursive, because they may wrap anything) +        # TODO: Handle these explicitly in handle() or make them iterable.          if isinstance(out, HTTPError):              out.apply(response)              out = self.error_handler.get(out.status, repr)(out) @@ -732,14 +821,13 @@ class Bottle(object):              environ['bottle.app'] = self              request.bind(environ)              response.bind() -            out = self._handle(environ) -            out = self._cast(out, request, response) +            out = self._cast(self._handle(environ), request, response)              # rfc2616 section 4.3 -            if response.status in (100, 101, 204, 304) or request.method == 'HEAD': +            if response._status_code in (100, 101, 204, 304)\ +            or request.method == 'HEAD':                  if hasattr(out, 'close'): out.close()                  out = [] -            status = '%d %s' % (response.status, HTTP_CODES[response.status]) -            start_response(status, response.headerlist) +            start_response(response._status_line, list(response.iter_headers()))              return out          except (KeyboardInterrupt, SystemExit, MemoryError):              raise @@ -755,6 +843,7 @@ class Bottle(object):              return [tob(err)]      def __call__(self, environ, start_response): +        ''' Each instance of :class:'Bottle' is a WSGI application. '''          return self.wsgi(environ, start_response) @@ -767,196 +856,136 @@ class Bottle(object):  ############################################################################### -class Request(threading.local, DictMixin): -    """ Represents a single HTTP request using thread-local attributes. -        The Request object wraps a WSGI environment and can be used as such. -    """ -    def __init__(self, environ=None): -        """ Create a new Request instance. - -            You usually don't do this but use the global `bottle.request` -            instance instead. -        """ -        self.bind(environ or {},) +class BaseRequest(DictMixin): +    """ A wrapper for WSGI environment dictionaries that adds a lot of +        convenient access methods and properties. Most of them are read-only.""" -    def bind(self, environ): -        """ Bind a new WSGI environment. +    #: Maximum size of memory buffer for :attr:`body` in bytes. +    MEMFILE_MAX = 102400 -            This is done automatically for the global `bottle.request` -            instance on every request. -        """ +    def __init__(self, environ): +        """ Wrap a WSGI environ dictionary. """ +        #: The wrapped WSGI environ dictionary. This is the only real attribute. +        #: All other attributes actually are read-only properties.          self.environ = environ -        # These attributes are used anyway, so it is ok to compute them here -        self.path = '/' + environ.get('PATH_INFO', '/').lstrip('/') -        self.method = environ.get('REQUEST_METHOD', 'GET').upper() - -    @property -    def _environ(self): -        depr("Request._environ renamed to Request.environ") -        return self.environ - -    def copy(self): -        ''' Returns a copy of self ''' -        return Request(self.environ.copy()) - -    def path_shift(self, shift=1): -        ''' Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. - -           :param shift: The number of path fragments to shift. May be negative -                         to change the shift direction. (default: 1) -        ''' -        script_name = self.environ.get('SCRIPT_NAME','/') -        self['SCRIPT_NAME'], self.path = path_shift(script_name, self.path, shift) -        self['PATH_INFO'] = self.path - -    def __getitem__(self, key): return self.environ[key] -    def __delitem__(self, key): self[key] = ""; del(self.environ[key]) -    def __iter__(self): return iter(self.environ) -    def __len__(self): return len(self.environ) -    def keys(self): return self.environ.keys() -    def __setitem__(self, key, value): -        """ Shortcut for Request.environ.__setitem__ """ -        self.environ[key] = value -        todelete = [] -        if key in ('PATH_INFO','REQUEST_METHOD'): -            self.bind(self.environ) -        elif key == 'wsgi.input': todelete = ('body','forms','files','params') -        elif key == 'QUERY_STRING': todelete = ('get','params') -        elif key.startswith('HTTP_'): todelete = ('headers', 'cookies') -        for key in todelete: -            if 'bottle.' + key in self.environ: -                del self.environ['bottle.' + key] - -    @DictProperty('environ', 'bottle.urlparts', read_only=True) -    def urlparts(self): -        ''' Return a :class:`urlparse.SplitResult` tuple that can be used -            to reconstruct the full URL as requested by the client. -            The tuple contains: (scheme, host, path, query_string, fragment). -            The fragment is always empty because it is not visible to the server. -        ''' -        env = self.environ -        host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST', '') -        http = env.get('wsgi.url_scheme', 'http') -        port = env.get('SERVER_PORT') -        if ':' in host: # Overrule SERVER_POST (proxy support) -            host, port = host.rsplit(':', 1) -        if not host or host == '127.0.0.1': -            host = env.get('SERVER_NAME', host) -        if port and http+port not in ('http80', 'https443'): -            host += ':' + port -        spath = self.environ.get('SCRIPT_NAME','').rstrip('/') + '/' -        rpath = self.path.lstrip('/') -        path = urlquote(urljoin(spath, rpath)) -        return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') - -    @property -    def url(self): -        """ Full URL as requested by the client. """ -        return self.urlparts.geturl() - -    @property -    def fullpath(self): -        """ Request path including SCRIPT_NAME (if present). """ -        return urlunquote(self.urlparts[2]) - -    @property -    def query_string(self): -        """ The part of the URL following the '?'. """ -        return self.environ.get('QUERY_STRING', '') +        environ['bottle.request'] = self      @property -    def content_length(self): -        """ Content-Length header as an integer, -1 if not specified """ -        return int(self.environ.get('CONTENT_LENGTH', '') or -1) +    def path(self): +        ''' The value of ``PATH_INFO`` with exactly one prefixed slash (to fix +            broken clients and avoid the "empty path" edge case). ''' +        return '/' + self.environ.get('PATH_INFO','').lstrip('/')      @property -    def header(self): -        depr("The Request.header property was renamed to Request.headers") -        return self.headers +    def method(self): +        ''' The ``REQUEST_METHOD`` value as an uppercase string. ''' +        return self.environ.get('REQUEST_METHOD', 'GET').upper() -    @DictProperty('environ', 'bottle.headers', read_only=True) +    @DictProperty('environ', 'bottle.request.headers', read_only=True)      def headers(self): -        ''' Request HTTP Headers stored in a :class:`HeaderDict`. ''' +        ''' A :class:`WSGIHeaderDict` that provides case-insensitive access to +            HTTP request headers. '''          return WSGIHeaderDict(self.environ) -    @DictProperty('environ', 'bottle.get', read_only=True) -    def GET(self): -        """ The QUERY_STRING parsed into an instance of :class:`MultiDict`. """ +    def get_header(self, name, default=None): +        ''' Return the value of a request header, or a given default value. ''' +        return self.headers.get(name, default) + +    @DictProperty('environ', 'bottle.request.cookies', read_only=True) +    def cookies(self): +        """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT +            decoded. Use :meth:`get_cookie` if you expect signed cookies. """ +        cookies = SimpleCookie(self.environ.get('HTTP_COOKIE','')) +        return FormsDict((c.key, c.value) for c in cookies.itervalues()) + +    def get_cookie(self, key, default=None, secret=None): +        """ Return the content of a cookie. To read a `Signed Cookie`, the +            `secret` must match the one used to create the cookie (see +            :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing +            cookie or wrong signature), return a default value. """ +        value = self.cookies.get(key) +        if secret and value: +            dec = cookie_decode(value, secret) # (key, value) tuple or None +            return dec[1] if dec and dec[0] == key else default +        return value or default + +    @DictProperty('environ', 'bottle.request.query', read_only=True) +    def query(self): +        ''' The :attr:`query_string` parsed into a :class:`FormsDict`. These +            values are sometimes called "URL arguments" or "GET parameters", but +            not to be confused with "URL wildcards" as they are provided by the +            :class:`Router`. '''          data = parse_qs(self.query_string, keep_blank_values=True) -        get = self.environ['bottle.get'] = MultiDict() +        get = self.environ['bottle.get'] = FormsDict()          for key, values in data.iteritems():              for value in values:                  get[key] = value          return get -    @DictProperty('environ', 'bottle.post', read_only=True) -    def POST(self): -        """ The combined values from :attr:`forms` and :attr:`files`. Values are -            either strings (form values) or instances of -            :class:`cgi.FieldStorage` (file uploads). -        """ -        post = MultiDict() -        safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi -        for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): -            if key in self.environ: safe_env[key] = self.environ[key] -        if NCTextIOWrapper: -            fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n') -        else: -            fb = self.body -        data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True) -        for item in data.list or []: -            post[item.name] = item if item.filename else item.value -        return post - -    @DictProperty('environ', 'bottle.forms', read_only=True) +    @DictProperty('environ', 'bottle.request.forms', read_only=True)      def forms(self): -        """ POST form values parsed into an instance of :class:`MultiDict`. - -            This property contains form values parsed from an `url-encoded` -            or `multipart/form-data` encoded POST request bidy. The values are -            native strings. -        """ -        forms = MultiDict() +        """ Form values parsed from an `url-encoded` or `multipart/form-data` +            encoded POST or PUT request body. The result is retuned as a +            :class:`FormsDict`. All keys and values are strings. File uploads +            are stored separately in :attr:`files`. """ +        forms = FormsDict()          for name, item in self.POST.iterallitems():              if not hasattr(item, 'filename'):                  forms[name] = item          return forms -    @DictProperty('environ', 'bottle.files', read_only=True) -    def files(self): -        """ File uploads parsed into an instance of :class:`MultiDict`. +    @DictProperty('environ', 'bottle.request.params', read_only=True) +    def params(self): +        """ A :class:`FormsDict` with the combined values of :attr:`query` and +            :attr:`forms`. File uploads are stored in :attr:`files`. """ +        params = FormsDict() +        for key, value in self.query.iterallitems(): +            params[key] = value +        for key, value in self.forms.iterallitems(): +            params[key] = value +        return params -            This property contains file uploads parsed from an -            `multipart/form-data` encoded POST request body. The values are -            instances of :class:`cgi.FieldStorage`. +    @DictProperty('environ', 'bottle.request.files', read_only=True) +    def files(self): +        """ File uploads parsed from an `url-encoded` or `multipart/form-data` +            encoded POST or PUT request body. The values are instances of +            :class:`cgi.FieldStorage`. The most important attributes are: + +            filename +                The filename, if specified; otherwise None; this is the client +                side filename, *not* the file name on which it is stored (that's +                a temporary file you don't deal with) +            file +                The file(-like) object from which you can read the data. +            value +                The value as a *string*; for file uploads, this transparently +                reads the file every time you request the value. Do not do this +                on big files.          """ -        files = MultiDict() +        files = FormsDict()          for name, item in self.POST.iterallitems():              if hasattr(item, 'filename'):                  files[name] = item          return files -    @DictProperty('environ', 'bottle.params', read_only=True) -    def params(self): -        """ A combined :class:`MultiDict` with values from :attr:`forms` and -            :attr:`GET`. File-uploads are not included. """ -        params = MultiDict(self.GET) -        for key, value in self.forms.iterallitems(): -            params[key] = value -        return params +    @DictProperty('environ', 'bottle.request.json', read_only=True) +    def json(self): +        ''' If the ``Content-Type`` header is ``application/json``, this +            property holds the parsed content of the request body. Only requests +            smaller than :attr:`MEMFILE_MAX` are processed to avoid memory +            exhaustion. ''' +        if 'application/json' in self.environ.get('CONTENT_TYPE', '') \ +        and 0 < self.content_length < self.MEMFILE_MAX: +            return json_loads(self.body.read(self.MEMFILE_MAX)) +        return None -    @DictProperty('environ', 'bottle.body', read_only=True) +    @DictProperty('environ', 'bottle.request.body', read_only=True)      def _body(self): -        """ The HTTP request body as a seekable file-like object. - -            This property returns a copy of the `wsgi.input` stream and should -            be used instead of `environ['wsgi.input']`. -         """          maxread = max(0, self.content_length)          stream = self.environ['wsgi.input'] -        body = BytesIO() if maxread < MEMFILE_MAX else TemporaryFile(mode='w+b') +        body = BytesIO() if maxread < self.MEMFILE_MAX else TemporaryFile(mode='w+b')          while maxread > 0: -            part = stream.read(min(maxread, MEMFILE_MAX)) +            part = stream.read(min(maxread, self.MEMFILE_MAX))              if not part: break              body.write(part)              maxread -= len(part) @@ -966,124 +995,378 @@ class Request(threading.local, DictMixin):      @property      def body(self): +        """ The HTTP request body as a seek-able file-like object. Depending on +            :attr:`MEMFILE_MAX`, this is either a temporary file or a +            :class:`io.BytesIO` instance. Accessing this property for the first +            time reads and replaces the ``wsgi.input`` environ variable. +            Subsequent accesses just do a `seek(0)` on the file object. """          self._body.seek(0)          return self._body -    @property -    def auth(self): #TODO: Tests and docs. Add support for digest. namedtuple? -        """ HTTP authorization data as a (user, passwd) tuple. (experimental) +    #: An alias for :attr:`query`. +    GET = query -            This implementation currently only supports basic auth and returns -            None on errors. +    @DictProperty('environ', 'bottle.request.post', read_only=True) +    def POST(self): +        """ The values of :attr:`forms` and :attr:`files` combined into a single +            :class:`FormsDict`. Values are either strings (form values) or +            instances of :class:`cgi.FieldStorage` (file uploads).          """ -        return parse_auth(self.headers.get('Authorization','')) +        post = FormsDict() +        safe_env = {'QUERY_STRING':''} # Build a safe environment for cgi +        for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): +            if key in self.environ: safe_env[key] = self.environ[key] +        if NCTextIOWrapper: +            fb = NCTextIOWrapper(self.body, encoding='ISO-8859-1', newline='\n') +        else: +            fb = self.body +        data = cgi.FieldStorage(fp=fb, environ=safe_env, keep_blank_values=True) +        for item in data.list or []: +            post[item.name] = item if item.filename else item.value +        return post -    @DictProperty('environ', 'bottle.cookies', read_only=True) +    @property      def COOKIES(self): -        """ Cookies parsed into a dictionary. Signed cookies are NOT decoded -            automatically. See :meth:`get_cookie` for details. -        """ -        raw_dict = SimpleCookie(self.headers.get('Cookie','')) -        cookies = {} -        for cookie in raw_dict.itervalues(): -            cookies[cookie.key] = cookie.value -        return cookies - -    def get_cookie(self, key, secret=None): -        """ Return the content of a cookie. To read a `Signed Cookies`, use the -            same `secret` as used to create the cookie (see -            :meth:`Response.set_cookie`). If anything goes wrong, None is -            returned. -        """ -        value = self.COOKIES.get(key) -        if secret and value: -            dec = cookie_decode(value, secret) # (key, value) tuple or None -            return dec[1] if dec and dec[0] == key else None -        return value or None +        ''' Alias for :attr:`cookies` (deprecated). ''' +        depr('BaseRequest.COOKIES was renamed to BaseRequest.cookies (lowercase).') +        return self.cookies      @property -    def is_ajax(self): -        ''' True if the request was generated using XMLHttpRequest ''' -        #TODO: write tests -        return self.header.get('X-Requested-With') == 'XMLHttpRequest' +    def url(self): +        """ The full request URI including hostname and scheme. If your app +            lives behind a reverse proxy or load balancer and you get confusing +            results, make sure that the ``X-Forwarded-Host`` header is set +            correctly. """ +        return self.urlparts.geturl() +    @DictProperty('environ', 'bottle.request.urlparts', read_only=True) +    def urlparts(self): +        ''' The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. +            The tuple contains (scheme, host, path, query_string and fragment), +            but the fragment is always empty because it is not visible to the +            server. ''' +        env = self.environ +        http = env.get('wsgi.url_scheme', 'http') +        host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') +        if not host: +            # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. +            host = env.get('SERVER_NAME', '127.0.0.1') +            port = env.get('SERVER_PORT') +            if port and port != ('80' if http == 'http' else '443'): +                host += ':' + port +        path = urlquote(self.fullpath) +        return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') -class Response(threading.local): -    """ Represents a single HTTP response using thread-local attributes. -    """ +    @property +    def fullpath(self): +        """ Request path including :attr:`script_name` (if present). """ +        return urljoin(self.script_name, self.path.lstrip('/')) -    def __init__(self): -        self.bind() +    @property +    def query_string(self): +        """ The raw :attr:`query` part of the URL (everything in between ``?`` +            and ``#``) as a string. """ +        return self.environ.get('QUERY_STRING', '') -    def bind(self): -        """ Resets the Response object to its factory defaults. """ -        self._COOKIES = None -        self.status = 200 -        self.headers = HeaderDict() -        self.content_type = 'text/html; charset=UTF-8' +    @property +    def script_name(self): +        ''' The initial portion of the URL's `path` that was removed by a higher +            level (server or routing middleware) before the application was +            called. This script path is returned with leading and tailing +            slashes. ''' +        script_name = self.environ.get('SCRIPT_NAME', '').strip('/') +        return '/' + script_name + '/' if script_name else '/' + +    def path_shift(self, shift=1): +        ''' Shift path segments from :attr:`path` to :attr:`script_name` and +            vice versa. + +           :param shift: The number of path segments to shift. May be negative +                         to change the shift direction. (default: 1) +        ''' +        script = self.environ.get('SCRIPT_NAME','/') +        self['SCRIPT_NAME'], self['PATH_INFO'] = path_shift(script, self.path, shift)      @property -    def header(self): -        depr("Response.header renamed to Response.headers") -        return self.headers +    def content_length(self): +        ''' The request body length as an integer. The client is responsible to +            set this header. Otherwise, the real length of the body is unknown +            and -1 is returned. In this case, :attr:`body` will be empty. ''' +        return int(self.environ.get('CONTENT_LENGTH') or -1) + +    @property +    def is_xhr(self): +        ''' True if the request was triggered by a XMLHttpRequest. This only +            works with JavaScript libraries that support the `X-Requested-With` +            header (most of the popular libraries do). ''' +        requested_with = self.environ.get('HTTP_X_REQUESTED_WITH','') +        return requested_with.lower() == 'xmlhttprequest' + +    @property +    def is_ajax(self): +        ''' Alias for :attr:`is_xhr`. "Ajax" is not the right term. ''' +        return self.is_xhr + +    @property +    def auth(self): +        """ HTTP authentication data as a (user, password) tuple. This +            implementation currently supports basic (not digest) authentication +            only. If the authentication happened at a higher level (e.g. in the +            front web-server or a middleware), the password field is None, but +            the user field is looked up from the ``REMOTE_USER`` environ +            variable. On any errors, None is returned. """ +        basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION','')) +        if basic: return basic +        ruser = self.environ.get('REMOTE_USER') +        if ruser: return (ruser, None) +        return None + +    @property +    def remote_route(self): +        """ A list of all IPs that were involved in this request, starting with +            the client IP and followed by zero or more proxies. This does only +            work if all proxies support the ```X-Forwarded-For`` header. Note +            that this information can be forged by malicious clients. """ +        proxy = self.environ.get('HTTP_X_FORWARDED_FOR') +        if proxy: return [ip.strip() for ip in proxy.split(',')] +        remote = self.environ.get('REMOTE_ADDR') +        return [remote] if remote else [] + +    @property +    def remote_addr(self): +        """ The client IP as a string. Note that this information can be forged +            by malicious clients. """ +        route = self.remote_route +        return route[0] if route else None + +    def copy(self): +        """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ +        return Request(self.environ.copy()) + +    def __getitem__(self, key): return self.environ[key] +    def __delitem__(self, key): self[key] = ""; del(self.environ[key]) +    def __iter__(self): return iter(self.environ) +    def __len__(self): return len(self.environ) +    def keys(self): return self.environ.keys() +    def __setitem__(self, key, value): +        """ Change an environ value and clear all caches that depend on it. """ + +        if self.environ.get('bottle.request.readonly'): +            raise KeyError('The environ dictionary is read-only.') + +        self.environ[key] = value +        todelete = () + +        if key == 'wsgi.input': +            todelete = ('body', 'forms', 'files', 'params', 'post', 'json') +        elif key == 'QUERY_STRING': +            todelete = ('query', 'params') +        elif key.startswith('HTTP_'): +            todelete = ('headers', 'cookies') + +        for key in todelete: +            self.environ.pop('bottle.request.'+key, None) + +    def __repr__(self): +        return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) + +def _hkey(s): +    return s.title().replace('_','-') + + +class HeaderProperty(object): +    def __init__(self, name, reader=None, writer=str, default=''): +        self.name, self.reader, self.writer, self.default = name, reader, writer, default +        self.__doc__ = 'Current value of the %r header.' % name.title() + +    def __get__(self, obj, cls): +        if obj is None: return self +        value = obj.headers.get(self.name) +        return self.reader(value) if (value and self.reader) else (value or self.default) + +    def __set__(self, obj, value): +        if self.writer: value = self.writer(value) +        obj.headers[self.name] = value + +    def __delete__(self, obj): +        if self.name in obj.headers: +            del obj.headers[self.name] + + +class BaseResponse(object): +    """ Storage class for a response body as well as headers and cookies. + +        This class does support dict-like case-insensitive item-access to +        headers, but is NOT a dict. Most notably, iterating over a response +        yields parts of the body and not the headers. +    """ + +    default_status = 200 +    default_content_type = 'text/html; charset=UTF-8' + +    # Header blacklist for specific response codes +    # (rfc2616 section 10.2.3 and 10.3.5) +    bad_headers = { +        204: set(('Content-Type',)), +        304: set(('Allow', 'Content-Encoding', 'Content-Language', +                  'Content-Length', 'Content-Range', 'Content-Type', +                  'Content-Md5', 'Last-Modified'))} + +    def __init__(self, body='', status=None, **headers): +        self._status_line = None +        self._status_code = None +        self.body = body +        self._cookies = None +        self._headers = {'Content-Type': [self.default_content_type]} +        self.status = status or self.default_status +        if headers: +            for name, value in headers.items(): +                self[name] = value      def copy(self):          ''' Returns a copy of self. '''          copy = Response()          copy.status = self.status -        copy.headers = self.headers.copy() -        copy.content_type = self.content_type +        copy._headers = dict((k, v[:]) for (k, v) in self._headers.items())          return copy +    def __iter__(self): +        return iter(self.body) + +    def close(self): +        if hasattr(self.body, 'close'): +            self.body.close() + +    @property +    def status_line(self): +        ''' The HTTP status line as a string (e.g. ``404 Not Found``).''' +        return self._status_line + +    @property +    def status_code(self): +        ''' The HTTP status code as an integer (e.g. 404).''' +        return self._status_code + +    def _set_status(self, status): +        if isinstance(status, int): +            code, status = status, _HTTP_STATUS_LINES.get(status) +        elif ' ' in status: +            status = status.strip() +            code   = int(status.split()[0]) +        else: +            raise ValueError('String status line without a reason phrase.') +        if not 100 <= code <= 999: raise ValueError('Status code out of range.') +        self._status_code = code +        self._status_line = status or ('%d Unknown' % code) + +    def _get_status(self): +        depr('BaseReuqest.status will change to return a string in 0.11. Use'\ +             ' status_line and status_code to make sure.') #0.10 +        return self._status_code + +    status = property(_get_status, _set_status, None, +        ''' A writeable property to change the HTTP response status. It accepts +            either a numeric code (100-999) or a string with a custom reason +            phrase (e.g. "404 Brain not found"). Both :data:`status_line` and +            :data:`status_code` are updates accordingly. The return value is +            always a numeric code. ''') +    del _get_status, _set_status + +    @property +    def headers(self): +        ''' An instance of :class:`HeaderDict`, a case-insensitive dict-like +            view on the response headers. ''' +        self.__dict__['headers'] = hdict = HeaderDict() +        hdict.dict = self._headers +        return hdict + +    def __contains__(self, name): return _hkey(name) in self._headers +    def __delitem__(self, name):  del self._headers[_hkey(name)] +    def __getitem__(self, name):  return self._headers[_hkey(name)][-1] +    def __setitem__(self, name, value): self._headers[_hkey(name)] = [str(value)] + +    def get_header(self, name, default=None): +        ''' Return the value of a previously defined header. If there is no +            header with that name, return a default value. ''' +        return self._headers.get(_hkey(name), [default])[-1] + +    def set_header(self, name, value, append=False): +        ''' Create a new response header, replacing any previously defined +            headers with the same name. ''' +        if append: +            self.add_header(name, value) +        else: +            self._headers[_hkey(name)] = [str(value)] + +    def add_header(self, name, value): +        ''' Add an additional response header, not removing duplicates. ''' +        self._headers.setdefault(_hkey(name), []).append(str(value)) + +    def iter_headers(self): +        ''' Yield (header, value) tuples, skipping headers that are not +            allowed with the current response status code. ''' +        headers = self._headers.iteritems() +        bad_headers = self.bad_headers.get(self.status_code) +        if bad_headers: +            headers = [h for h in headers if h[0] not in bad_headers] +        for name, values in headers: +            for value in values: +                yield name, value +        if self._cookies: +            for c in self._cookies.values(): +                yield 'Set-Cookie', c.OutputString() +      def wsgiheader(self): -        ''' Returns a wsgi conform list of header/value pairs. ''' -        for c in self.COOKIES.values(): -            if c.OutputString() not in self.headers.getall('Set-Cookie'): -                self.headers.append('Set-Cookie', c.OutputString()) -        # rfc2616 section 10.2.3, 10.3.5 -        if self.status in (204, 304) and 'content-type' in self.headers: -            del self.headers['content-type'] -        if self.status == 304: -            for h in ('allow', 'content-encoding', 'content-language', -                      'content-length', 'content-md5', 'content-range', -                      'content-type', 'last-modified'): # + c-location, expires? -                if h in self.headers: -                     del self.headers[h] -        return list(self.headers.iterallitems()) -    headerlist = property(wsgiheader) +        depr('The wsgiheader method is deprecated. See headerlist.') #0.10 +        return self.headerlist      @property -    def charset(self): -        """ Return the charset specified in the content-type header. +    def headerlist(self): +        ''' WSGI conform list of (header, value) tuples. ''' +        return list(self.iter_headers()) -            This defaults to `UTF-8`. -        """ +    content_type = HeaderProperty('Content-Type') +    content_length = HeaderProperty('Content-Length', reader=int) + +    @property +    def charset(self): +        """ Return the charset specified in the content-type header (default: utf8). """          if 'charset=' in self.content_type:              return self.content_type.split('charset=')[-1].split(';')[0].strip()          return 'UTF-8'      @property      def COOKIES(self): -        """ A dict-like SimpleCookie instance. Use :meth:`set_cookie` instead. """ -        if not self._COOKIES: -            self._COOKIES = SimpleCookie() -        return self._COOKIES - -    def set_cookie(self, key, value, secret=None, **kargs): -        ''' Add a cookie or overwrite an old one. If the `secret` parameter is +        """ A dict-like SimpleCookie instance. This should not be used directly. +            See :meth:`set_cookie`. """ +        depr('The COOKIES dict is deprecated. Use `set_cookie()` instead.') # 0.10 +        if not self._cookies: +            self._cookies = SimpleCookie() +        return self._cookies + +    def set_cookie(self, name, value, secret=None, **options): +        ''' Create a new cookie or replace an old one. If the `secret` parameter is              set, create a `Signed Cookie` (described below). -            :param key: the name of the cookie. +            :param name: the name of the cookie.              :param value: the value of the cookie. -            :param secret: required for signed cookies. (default: None) +            :param secret: a signature key required for signed cookies. + +            Additionally, this method accepts all RFC 2109 attributes that are +            supported by :class:`cookie.Morsel`, including: +              :param max_age: maximum age in seconds. (default: None) -            :param expires: a datetime object or UNIX timestamp. (defaut: None) +            :param expires: a datetime object or UNIX timestamp. (default: None)              :param domain: the domain that is allowed to read the cookie.                (default: current domain) -            :param path: limits the cookie to a given path (default: /) +            :param path: limits the cookie to a given path (default: current path) +            :param secure: limit the cookie to HTTPS connections (default: off). +            :param httponly: prevents client-side javascript to read this cookie +              (default: off, requires Python 2.6 or newer). -            If neither `expires` nor `max_age` are set (default), the cookie -            lasts only as long as the browser is not closed. +            If neither `expires` nor `max_age` is set (default), the cookie will +            expire at the end of the browser session (as soon as the browser +            window is closed).              Signed cookies may store any pickle-able object and are              cryptographically signed to prevent manipulation. Keep in mind that @@ -1094,31 +1377,55 @@ class Response(threading.local):              cookie). The main intention is to make pickling and unpickling              save, not to store secret information at client side.          ''' +        if not self._cookies: +            self._cookies = SimpleCookie() +          if secret: -            value = touni(cookie_encode((key, value), secret)) +            value = touni(cookie_encode((name, value), secret))          elif not isinstance(value, basestring): -            raise TypeError('Secret missing for non-string Cookie.') - -        self.COOKIES[key] = value -        for k, v in kargs.iteritems(): -            self.COOKIES[key][k.replace('_', '-')] = v +            raise TypeError('Secret key missing for non-string Cookie.') + +        if len(value) > 4096: raise ValueError('Cookie value to long.') +        self._cookies[name] = value + +        for key, value in options.iteritems(): +            if key == 'max_age': +                if isinstance(value, timedelta): +                    value = value.seconds + value.days * 24 * 3600 +            if key == 'expires': +                if isinstance(value, (datedate, datetime)): +                    value = value.timetuple() +                elif isinstance(value, (int, float)): +                    value = time.gmtime(value) +                value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) +            self._cookies[name][key.replace('_', '-')] = value      def delete_cookie(self, key, **kwargs):          ''' Delete a cookie. Be sure to use the same `domain` and `path` -            parameters as used to create the cookie. ''' +            settings as used to create the cookie. '''          kwargs['max_age'] = -1          kwargs['expires'] = 0          self.set_cookie(key, '', **kwargs) -    def get_content_type(self): -        """ Current 'Content-Type' header. """ -        return self.headers['Content-Type'] +    def __repr__(self): +        out = '' +        for name, value in self.headerlist: +            out += '%s: %s\n' % (name.title(), value.strip()) +        return out + -    def set_content_type(self, value): -        self.headers['Content-Type'] = value +class LocalRequest(BaseRequest, threading.local): +    ''' A thread-local subclass of :class:`BaseRequest`. ''' +    def __init__(self): pass +    bind = BaseRequest.__init__ -    content_type = property(get_content_type, set_content_type, None, -                            get_content_type.__doc__) + +class LocalResponse(BaseResponse, threading.local): +    ''' A thread-local subclass of :class:`BaseResponse`. ''' +    bind = BaseResponse.__init__ + +Response = LocalResponse # BC 0.9 +Request  = LocalRequest  # BC 0.9 @@ -1129,10 +1436,11 @@ class Response(threading.local):  # Plugins ######################################################################  ############################################################################### - +class PluginError(BottleException): pass  class JSONPlugin(object):      name = 'json' +    api  = 2      def __init__(self, json_dumps=json_dumps):          self.json_dumps = json_dumps @@ -1143,18 +1451,23 @@ class JSONPlugin(object):          def wrapper(*a, **ka):              rv = callback(*a, **ka)              if isinstance(rv, dict): +                #Attempt to serialize, raises exception on failure +                json_response = dumps(rv) +                #Set content type only if serialization succesful                  response.content_type = 'application/json' -                return dumps(rv) +                return json_response              return rv          return wrapper -  class HooksPlugin(object):      name = 'hooks' +    api  = 2 + +    _names = 'before_request', 'after_request', 'app_reset'      def __init__(self): -        self.hooks = {'before_request': [], 'after_request': []} +        self.hooks = dict((name, []) for name in self._names)          self.app = None      def _empty(self): @@ -1165,56 +1478,29 @@ class HooksPlugin(object):      def add(self, name, func):          ''' Attach a callback to a hook. ''' -        if name not in self.hooks: -            raise ValueError("Unknown hook name %s" % name)          was_empty = self._empty() -        self.hooks[name].append(func) +        self.hooks.setdefault(name, []).append(func)          if self.app and was_empty and not self._empty(): self.app.reset()      def remove(self, name, func):          ''' Remove a callback from a hook. ''' -        if name not in self.hooks: -            raise ValueError("Unknown hook name %s" % name)          was_empty = self._empty() -        self.hooks[name].remove(func) +        if name in self.hooks and func in self.hooks[name]: +            self.hooks[name].remove(func)          if self.app and not was_empty and self._empty(): self.app.reset() -    def apply(self, callback, context): -        if self._empty(): return callback -        before_request = self.hooks['before_request'] -        after_request  = self.hooks['after_request'] -        def wrapper(*a, **ka): -            for hook in before_request: hook() -            rv = callback(*a, **ka) -            for hook in after_request[::-1]: hook() -            return rv -        return wrapper - - - -class TypeFilterPlugin(object): -    def __init__(self): -        self.filter = [] -        self.app = None - -    def setup(self, app): -        self.app = app - -    def add(self, ftype, func): -        if not isinstance(ftype, type): -            raise TypeError("Expected type object, got %s" % type(ftype)) -        self.filter = [(t, f) for (t, f) in self.filter if t != ftype] -        self.filter.append((ftype, func)) -        if len(self.filter) == 1 and self.app: self.app.reset() +    def trigger(self, name, *a, **ka): +        ''' Trigger a hook and return a list of results. ''' +        hooks = self.hooks[name] +        if ka.pop('reversed', False): hooks = hooks[::-1] +        return [hook(*a, **ka) for hook in hooks]      def apply(self, callback, context): -        filter = self.filter -        if not filter: return callback +        if self._empty(): return callback          def wrapper(*a, **ka): +            self.trigger('before_request')              rv = callback(*a, **ka) -            for testtype, filterfunc in filter: -                if isinstance(rv, testtype): -                    rv = filterfunc(rv) +            self.trigger('after_request', reversed=True)              return rv          return wrapper @@ -1225,14 +1511,15 @@ class TemplatePlugin(object):          element must be a dict with additional options (e.g. `template_engine`)          or default variables for the template. '''      name = 'template' +    api  = 2 -    def apply(self, callback, context): -        conf = context['config'].get('template') +    def apply(self, callback, route): +        conf = route.config.get('template')          if isinstance(conf, (tuple, list)) and len(conf) == 2:              return view(conf[0], **conf[1])(callback) -        elif isinstance(conf, str) and 'template_opts' in context['config']: +        elif isinstance(conf, str) and 'template_opts' in route.config:              depr('The `template_opts` parameter is deprecated.') #0.9 -            return view(conf, **context['config']['template_opts'])(callback) +            return view(conf, **route.config['template_opts'])(callback)          elif isinstance(conf, str):              return view(conf)(callback)          else: @@ -1246,7 +1533,7 @@ class _ImportRedirect(object):          self.name = name          self.impmask = impmask          self.module = sys.modules.setdefault(name, imp.new_module(name)) -        self.module.__dict__.update({'__file__': '<virtual>', '__path__': [], +        self.module.__dict__.update({'__file__': __file__, '__path__': [],                                      '__all__': [], '__loader__': self})          sys.meta_path.append(self) @@ -1277,53 +1564,115 @@ class _ImportRedirect(object):  class MultiDict(DictMixin): -    """ A dict that remembers old values for each key """ -    # collections.MutableMapping would be better for Python >= 2.6 -    def __init__(self, *a, **k): -        self.dict = dict() -        for k, v in dict(*a, **k).iteritems(): -            self[k] = v +    """ This dict stores multiple values per key, but behaves exactly like a +        normal dict in that it returns only the newest value for any given key. +        There are special methods available to access the full list of values. +    """ +    def __init__(self, *a, **k): +        self.dict = dict((k, [v]) for k, v in dict(*a, **k).iteritems())      def __len__(self): return len(self.dict)      def __iter__(self): return iter(self.dict)      def __contains__(self, key): return key in self.dict      def __delitem__(self, key): del self.dict[key] -    def keys(self): return self.dict.keys() -    def __getitem__(self, key): return self.get(key, KeyError, -1) +    def __getitem__(self, key): return self.dict[key][-1]      def __setitem__(self, key, value): self.append(key, value) - -    def append(self, key, value): self.dict.setdefault(key, []).append(value) -    def replace(self, key, value): self.dict[key] = [value] -    def getall(self, key): return self.dict.get(key) or [] - -    def get(self, key, default=None, index=-1): -        if key not in self.dict and default != KeyError: -            return [default][index] -        return self.dict[key][index] - +    def iterkeys(self): return self.dict.iterkeys() +    def itervalues(self): return (v[-1] for v in self.dict.itervalues()) +    def iteritems(self): return ((k, v[-1]) for (k, v) in self.dict.iteritems())      def iterallitems(self):          for key, values in self.dict.iteritems():              for value in values:                  yield key, value +    # 2to3 is not able to fix these automatically. +    keys     = iterkeys     if py3k else lambda self: list(self.iterkeys()) +    values   = itervalues   if py3k else lambda self: list(self.itervalues()) +    items    = iteritems    if py3k else lambda self: list(self.iteritems()) +    allitems = iterallitems if py3k else lambda self: list(self.iterallitems()) + +    def get(self, key, default=None, index=-1, type=None): +        ''' Return the most recent value for a key. + +            :param default: The default value to be returned if the key is not +                   present or the type conversion fails. +            :param index: An index for the list of available values. +            :param type: If defined, this callable is used to cast the value +                    into a specific type. Exception are suppressed and result in +                    the default value to be returned. +        ''' +        try: +            val = self.dict[key][index] +            return type(val) if type else val +        except Exception, e: +            pass +        return default -class HeaderDict(MultiDict): -    """ Same as :class:`MultiDict`, but title()s the keys and overwrites. """ -    def __contains__(self, key): -        return MultiDict.__contains__(self, self.httpkey(key)) -    def __getitem__(self, key): -        return MultiDict.__getitem__(self, self.httpkey(key)) -    def __delitem__(self, key): -        return MultiDict.__delitem__(self, self.httpkey(key)) -    def __setitem__(self, key, value): self.replace(key, value) -    def get(self, key, default=None, index=-1): -        return MultiDict.get(self, self.httpkey(key), default, index)      def append(self, key, value): -        return MultiDict.append(self, self.httpkey(key), str(value)) +        ''' Add a new value to the list of values for this key. ''' +        self.dict.setdefault(key, []).append(value) +      def replace(self, key, value): -        return MultiDict.replace(self, self.httpkey(key), str(value)) -    def getall(self, key): return MultiDict.getall(self, self.httpkey(key)) -    def httpkey(self, key): return str(key).replace('_','-').title() +        ''' Replace the list of values with a single value. ''' +        self.dict[key] = [value] + +    def getall(self, key): +        ''' Return a (possibly empty) list of values for a key. ''' +        return self.dict.get(key) or [] + +    #: Aliases for WTForms to mimic other multi-dict APIs (Django) +    getone = get +    getlist = getall + + + +class FormsDict(MultiDict): +    ''' This :class:`MultiDict` subclass is used to store request form data. +        Additionally to the normal dict-like item access methods (which return +        unmodified data as native strings), this container also supports +        attribute-like access to its values. Attribues are automatiically de- or +        recoded to match :attr:`input_encoding` (default: 'utf8'). Missing +        attributes default to an empty string. ''' + +    #: Encoding used for attribute values. +    input_encoding = 'utf8' + +    def getunicode(self, name, default=None, encoding=None): +        value, enc = self.get(name, default), encoding or self.input_encoding +        try: +            if isinstance(value, bytes): # Python 2 WSGI +                return value.decode(enc) +            elif isinstance(value, unicode): # Python 3 WSGI +                return value.encode('latin1').decode(enc) +            return value +        except UnicodeError, e: +            return default + +    def __getattr__(self, name): return self.getunicode(name, default=u'') + + +class HeaderDict(MultiDict): +    """ A case-insensitive version of :class:`MultiDict` that defaults to +        replace the old value instead of appending it. """ + +    def __init__(self, *a, **ka): +        self.dict = {} +        if a or ka: self.update(*a, **ka) + +    def __contains__(self, key): return _hkey(key) in self.dict +    def __delitem__(self, key): del self.dict[_hkey(key)] +    def __getitem__(self, key): return self.dict[_hkey(key)][-1] +    def __setitem__(self, key, value): self.dict[_hkey(key)] = [str(value)] +    def append(self, key, value): +        self.dict.setdefault(_hkey(key), []).append(str(value)) +    def replace(self, key, value): self.dict[_hkey(key)] = [str(value)] +    def getall(self, key): return self.dict.get(_hkey(key)) or [] +    def get(self, key, default=None, index=-1): +        return MultiDict.get(self, _hkey(key), default, index) +    def filter(self, names): +        for name in map(_hkey, names): +            if name in self.dict: +                del self.dict[name]  class WSGIHeaderDict(DictMixin): @@ -1370,11 +1719,44 @@ class WSGIHeaderDict(DictMixin):              elif key in self.cgikeys:                  yield key.replace('_', '-').title() -    def keys(self): return list(self) -    def __len__(self): return len(list(self)) +    def keys(self): return [x for x in self] +    def __len__(self): return len(self.keys())      def __contains__(self, key): return self._ekey(key) in self.environ +class ConfigDict(dict): +    ''' A dict-subclass with some extras: You can access keys like attributes. +        Uppercase attributes create new ConfigDicts and act as name-spaces. +        Other missing attributes return None. Calling a ConfigDict updates its +        values and returns itself. + +        >>> cfg = ConfigDict() +        >>> cfg.Namespace.value = 5 +        >>> cfg.OtherNamespace(a=1, b=2) +        >>> cfg +        {'Namespace': {'value': 5}, 'OtherNamespace': {'a': 1, 'b': 2}} +    ''' + +    def __getattr__(self, key): +        if key not in self and key[0].isupper(): +            self[key] = ConfigDict() +        return self.get(key) + +    def __setattr__(self, key, value): +        if hasattr(dict, key): +            raise AttributeError('Read-only attribute.') +        if key in self and self[key] and isinstance(self[key], ConfigDict): +            raise AttributeError('Non-empty namespace attribute.') +        self[key] = value + +    def __delattr__(self, key): +        if key in self: del self[key] + +    def __call__(self, *a, **ka): +        for key, value in dict(*a, **ka).iteritems(): setattr(self, key, value) +        return self + +  class AppStack(list):      """ A stack-like list. Calling it returns the head of the stack. """ @@ -1414,30 +1796,21 @@ class WSGIFileWrapper(object):  ############################################################################### -def dict2json(d): -    depr('JSONPlugin is the preferred way to return JSON.') #0.9 -    response.content_type = 'application/json' -    return json_dumps(d) - -  def abort(code=500, text='Unknown Error: Application stopped.'):      """ Aborts execution and causes a HTTP error. """      raise HTTPError(code, text) -def redirect(url, code=303): -    """ Aborts execution and causes a 303 redirect. """ +def redirect(url, code=None): +    """ Aborts execution and causes a 303 or 302 redirect, depending on +        the HTTP protocol version. """ +    if code is None: +        code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302      location = urljoin(request.url, url)      raise HTTPResponse("", status=code, header=dict(Location=location)) -def send_file(*a, **k): #BC 0.6.4 -    """ Raises the output of static_file(). (deprecated) """ -    depr("Use 'raise static_file()' instead of 'send_file()'.") -    raise static_file(*a, **k) - - -def static_file(filename, root, mimetype='auto', guessmime=True, download=False): +def static_file(filename, root, mimetype='auto', download=False):      """ Open a file in a safe way and return :exc:`HTTPResponse` with status          code 200, 305, 401 or 404. Set Content-Type, Content-Encoding,          Content-Length and Last-Modified header. Obey If-Modified-Since header @@ -1454,9 +1827,6 @@ def static_file(filename, root, mimetype='auto', guessmime=True, download=False)      if not os.access(filename, os.R_OK):          return HTTPError(403, "You do not have permission to access this file.") -    if not guessmime: #0.9 -       if mimetype == 'auto': mimetype = 'text/plain' -       depr("To disable mime-type guessing, specify a type explicitly.")      if mimetype == 'auto':          mimetype, encoding = mimetypes.guess_type(filename)          if mimetype: header['Content-Type'] = mimetype @@ -1514,9 +1884,10 @@ def parse_auth(header):      try:          method, data = header.split(None, 1)          if method.lower() == 'basic': -            name, pwd = base64.b64decode(data).split(':', 1) -            return name, pwd -    except (KeyError, ValueError, TypeError): +            #TODO: Add 2to3 save base64[encode/decode] functions. +            user, pwd = touni(base64.b64decode(tob(data))).split(':',1) +            return user, pwd +    except (KeyError, ValueError):          return None @@ -1529,7 +1900,7 @@ def _lscmp(a, b):  def cookie_encode(data, key):      ''' Encode and sign a pickle-able object. Return a (byte) string '''      msg = base64.b64encode(pickle.dumps(data, -1)) -    sig = base64.b64encode(hmac.new(key, msg).digest()) +    sig = base64.b64encode(hmac.new(tob(key), msg).digest())      return tob('!') + sig + tob('?') + msg @@ -1538,7 +1909,7 @@ def cookie_decode(data, key):      data = tob(data)      if cookie_is_encoded(data):          sig, msg = data.split(tob('?'), 1) -        if _lscmp(sig[1:], base64.b64encode(hmac.new(key, msg).digest())): +        if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())):              return pickle.loads(base64.b64decode(msg))      return None @@ -1548,6 +1919,18 @@ def cookie_is_encoded(data):      return bool(data.startswith(tob('!')) and tob('?') in data) +def html_escape(string): +    ''' Escape HTML special characters ``&<>`` and quotes ``'"``. ''' +    return string.replace('&','&').replace('<','<').replace('>','>')\ +                 .replace('"','"').replace("'",''') + + +def html_quote(string): +    ''' Escape and quote a string to be used as an HTTP attribute.''' +    return '"%s"' % html_escape(string).replace('\n','%#10;')\ +                    .replace('\r','
').replace('\t','	') + +  def yieldroutes(func):      """ Return a generator for routes that match the signature (name, args)      of the func parameter. This may yield more than one route if the function @@ -1600,17 +1983,15 @@ def path_shift(script_name, path_info, shift=1):      return new_script_name, new_path_info - -# Decorators -#TODO: Replace default_app() with app() -  def validate(**vkargs):      """      Validates and manipulates keyword arguments by user defined callables.      Handles ValueError and missing arguments by raising HTTPError(403).      """ +    depr('Use route wildcard filters instead.')      def decorator(func): -        def wrapper(**kargs): +        @functools.wraps(func) +        def wrapper(*args, **kargs):              for key, value in vkargs.iteritems():                  if key not in kargs:                      abort(403, 'Missing parameter: %s' % key) @@ -1618,7 +1999,7 @@ def validate(**vkargs):                      kargs[key] = value(kargs[key])                  except ValueError:                      abort(403, 'Wrong parameter format for: %s' % key) -            return func(**kargs) +            return func(*args, **kargs)          return wrapper      return decorator @@ -1652,11 +2033,6 @@ url = make_default_app_wrapper('get_url')  del name -def default(): -    depr("The default() decorator is deprecated. Use @error(404) instead.") -    return error(404) - - @@ -1685,15 +2061,17 @@ class CGIServer(ServerAdapter):      quiet = True      def run(self, handler): # pragma: no cover          from wsgiref.handlers import CGIHandler -        CGIHandler().run(handler) # Just ignore host and port here +        def fixed_environ(environ, start_response): +            environ.setdefault('PATH_INFO', '') +            return handler(environ, start_response) +        CGIHandler().run(fixed_environ)  class FlupFCGIServer(ServerAdapter):      def run(self, handler): # pragma: no cover          import flup.server.fcgi -        kwargs = {'bindAddress':(self.host, self.port)} -        kwargs.update(self.options) # allow to override bindAddress and others -        flup.server.fcgi.WSGIServer(handler, **kwargs).run() +        self.options.setdefault('bindAddress', (self.host, self.port)) +        flup.server.fcgi.WSGIServer(handler, **self.options).run()  class WSGIRefServer(ServerAdapter): @@ -1711,7 +2089,10 @@ class CherryPyServer(ServerAdapter):      def run(self, handler): # pragma: no cover          from cherrypy import wsgiserver          server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler) -        server.start() +        try: +            server.start() +        finally: +            server.stop()  class PasteServer(ServerAdapter): @@ -1723,6 +2104,7 @@ class PasteServer(ServerAdapter):          httpserver.serve(handler, host=self.host, port=str(self.port),                           **self.options) +  class MeinheldServer(ServerAdapter):      def run(self, handler):          from meinheld import server @@ -1755,9 +2137,7 @@ class FapwsServer(ServerAdapter):  class TornadoServer(ServerAdapter):      """ The super hyped asynchronous server by facebook. Untested. """      def run(self, handler): # pragma: no cover -        import tornado.wsgi -        import tornado.httpserver -        import tornado.ioloop +        import tornado.wsgi, tornado.httpserver, tornado.ioloop          container = tornado.wsgi.WSGIContainer(handler)          server = tornado.httpserver.HTTPServer(container)          server.listen(port=self.port) @@ -1807,22 +2187,29 @@ class GeventServer(ServerAdapter):            issues: No streaming, no pipelining, no SSL.      """      def run(self, handler): -        from gevent import wsgi as wsgi_fast, pywsgi as wsgi, monkey +        from gevent import wsgi as wsgi_fast, pywsgi, monkey, local          if self.options.get('monkey', True): -            monkey.patch_all() -        if self.options.get('fast', False): -            wsgi = wsgi_fast +            if not threading.local is local.local: monkey.patch_all() +        wsgi = wsgi_fast if self.options.get('fast') else pywsgi          wsgi.WSGIServer((self.host, self.port), handler).serve_forever()  class GunicornServer(ServerAdapter): -    """ Untested. """ +    """ Untested. See http://gunicorn.org/configure.html for options. """      def run(self, handler): -        from gunicorn.arbiter import Arbiter -        from gunicorn.config import Config -        handler.cfg = Config({'bind': "%s:%d" % (self.host, self.port), 'workers': 4}) -        arbiter = Arbiter(handler) -        arbiter.run() +        from gunicorn.app.base import Application + +        config = {'bind': "%s:%d" % (self.host, int(self.port))} +        config.update(self.options) + +        class GunicornApplication(Application): +            def init(self, parser, opts, args): +                return config + +            def load(self): +                return handler + +        GunicornApplication().run()  class EventletServer(ServerAdapter): @@ -1833,8 +2220,7 @@ class EventletServer(ServerAdapter):  class RocketServer(ServerAdapter): -    """ Untested. As requested in issue 63 -        https://github.com/defnull/bottle/issues/#issue/63 """ +    """ Untested. """      def run(self, handler):          from rocket import Rocket          server = Rocket((self.host, self.port), 'wsgi', { 'wsgi_app' : handler }) @@ -1842,7 +2228,7 @@ class RocketServer(ServerAdapter):  class BjoernServer(ServerAdapter): -    """ Screamingly fast server written in C: https://github.com/jonashaag/bjoern """ +    """ Fast server written in C: https://github.com/jonashaag/bjoern """      def run(self, handler):          from bjoern import run          run(handler, self.host, self.port) @@ -1858,7 +2244,6 @@ class AutoServer(ServerAdapter):              except ImportError:                  pass -  server_names = {      'cgi': CGIServer,      'flup': FlupFCGIServer, @@ -1889,57 +2274,41 @@ server_names = {  ############################################################################### -def _load(target, **vars): -    """ Fetch something from a module. The exact behaviour depends on the the -        target string: - -        If the target is a valid python import path (e.g. `package.module`), -        the rightmost part is returned as a module object. -        If the target contains a colon (e.g. `package.module:var`) the module -        variable specified after the colon is returned. -        If the part after the colon contains any non-alphanumeric characters -        (e.g. `package.module:func(var)`) the result of the expression -        is returned. The expression has access to keyword arguments supplied -        to this function. +def load(target, **namespace): +    """ Import a module or fetch an object from a module. -        Example:: -        >>> _load('bottle') -        <module 'bottle' from 'bottle.py'> -        >>> _load('bottle:Bottle') -        <class 'bottle.Bottle'> -        >>> _load('bottle:cookie_encode(v, secret)', v='foo', secret='bar') -        '!F+hN4dQxaDJ4QxxaZ+Z3jw==?gAJVA2Zvb3EBLg==' +        * ``package.module`` returns `module` as a module object. +        * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. +        * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. +        The last form accepts not only function calls, but any type of +        expression. Keyword arguments passed to this function are available as +        local variables. Example: ``import_string('re:compile(x)', x='[a-z]')``      """      module, target = target.split(":", 1) if ':' in target else (target, None) -    if module not in sys.modules: -        __import__(module) -    if not target: -        return sys.modules[module] -    if target.isalnum(): -        return getattr(sys.modules[module], target) +    if module not in sys.modules: __import__(module) +    if not target: return sys.modules[module] +    if target.isalnum(): return getattr(sys.modules[module], target)      package_name = module.split('.')[0] -    vars[package_name] = sys.modules[package_name] -    return eval('%s.%s' % (module, target), vars) +    namespace[package_name] = sys.modules[package_name] +    return eval('%s.%s' % (module, target), namespace)  def load_app(target): -    """ Load a bottle application based on a target string and return the -        application object. - -        If the target is an import path (e.g. package.module), the application -        stack is used to isolate the routes defined in that module. -        If the target contains a colon (e.g. package.module:myapp) the -        module variable specified after the colon is returned instead. -    """ -    tmp = app.push() # Create a new "default application" -    rv = _load(target) # Import the target module -    app.remove(tmp) # Remove the temporary added default application -    return rv if isinstance(rv, Bottle) else tmp - +    """ Load a bottle application from a module and make sure that the import +        does not affect the current default application, but returns a separate +        application object. See :func:`load` for the target parameter. """ +    global NORUN; NORUN, nr_old = True, NORUN +    try: +        tmp = default_app.push() # Create a new "default application" +        rv = load(target) # Import the target module +        return rv if callable(rv) else tmp +    finally: +        default_app.remove(tmp) # Remove the temporary added default application +        NORUN = nr_old  def run(app=None, server='wsgiref', host='127.0.0.1', port=8080, -        interval=1, reloader=False, quiet=False, **kargs): +        interval=1, reloader=False, quiet=False, plugins=None, **kargs):      """ Start a server instance. This method blocks until the server terminates.          :param app: WSGI application or target string supported by @@ -1956,114 +2325,115 @@ def run(app=None, server='wsgiref', host='127.0.0.1', port=8080,          :param quiet: Suppress output to stdout and stderr? (default: False)          :param options: Options passed to the server adapter.       """ -    app = app or default_app() -    if isinstance(app, basestring): -        app = load_app(app) -    if isinstance(server, basestring): -        server = server_names.get(server) -    if isinstance(server, type): -        server = server(host=host, port=port, **kargs) -    if not isinstance(server, ServerAdapter): -        raise RuntimeError("Server must be a subclass of ServerAdapter") -    server.quiet = server.quiet or quiet -    if not server.quiet and not os.environ.get('BOTTLE_CHILD'): -        print "Bottle server starting up (using %s)..." % repr(server) -        print "Listening on http://%s:%d/" % (server.host, server.port) -        print "Use Ctrl-C to quit." -        print +    if NORUN: return +    if reloader and not os.environ.get('BOTTLE_CHILD'): +        try: +            fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') +            os.close(fd) # We only need this file to exist. We never write to it +            while os.path.exists(lockfile): +                args = [sys.executable] + sys.argv +                environ = os.environ.copy() +                environ['BOTTLE_CHILD'] = 'true' +                environ['BOTTLE_LOCKFILE'] = lockfile +                p = subprocess.Popen(args, env=environ) +                while p.poll() is None: # Busy wait... +                    os.utime(lockfile, None) # I am alive! +                    time.sleep(interval) +                if p.poll() != 3: +                    if os.path.exists(lockfile): os.unlink(lockfile) +                    sys.exit(p.poll()) +        except KeyboardInterrupt: +            pass +        finally: +            if os.path.exists(lockfile): +                os.unlink(lockfile) +        return + +    stderr = sys.stderr.write +      try: +        app = app or default_app() +        if isinstance(app, basestring): +            app = load_app(app) +        if not callable(app): +            raise ValueError("Application is not callable: %r" % app) + +        for plugin in plugins or []: +            app.install(plugin) + +        if server in server_names: +            server = server_names.get(server) +        if isinstance(server, basestring): +            server = load(server) +        if isinstance(server, type): +            server = server(host=host, port=port, **kargs) +        if not isinstance(server, ServerAdapter): +            raise ValueError("Unknown or unsupported server: %r" % server) + +        server.quiet = server.quiet or quiet +        if not server.quiet: +            stderr("Bottle server starting up (using %s)...\n" % repr(server)) +            stderr("Listening on http://%s:%d/\n" % (server.host, server.port)) +            stderr("Hit Ctrl-C to quit.\n\n") +          if reloader: -            interval = min(interval, 1) -            if os.environ.get('BOTTLE_CHILD'): -                _reloader_child(server, app, interval) -            else: -                _reloader_observer(server, app, interval) +            lockfile = os.environ.get('BOTTLE_LOCKFILE') +            bgcheck = FileCheckerThread(lockfile, interval) +            with bgcheck: +                server.run(app) +            if bgcheck.status == 'reload': +                sys.exit(3)          else:              server.run(app)      except KeyboardInterrupt:          pass -    if not server.quiet and not os.environ.get('BOTTLE_CHILD'): -        print "Shutting down..." +    except (SyntaxError, ImportError): +        if not reloader: raise +        if not getattr(server, 'quiet', False): print_exc() +        sys.exit(3) +    finally: +        if not getattr(server, 'quiet', False): stderr('Shutdown...\n')  class FileCheckerThread(threading.Thread): -    ''' Thread that periodically checks for changed module files. ''' +    ''' Interrupt main-thread as soon as a changed module file is detected, +        the lockfile gets deleted or gets to old. '''      def __init__(self, lockfile, interval):          threading.Thread.__init__(self)          self.lockfile, self.interval = lockfile, interval -        #1: lockfile to old; 2: lockfile missing -        #3: module file changed; 5: external exit -        self.status = 0 +        #: Is one of 'reload', 'error' or 'exit' +        self.status = None      def run(self):          exists = os.path.exists          mtime = lambda path: os.stat(path).st_mtime          files = dict() +          for module in sys.modules.values():              path = getattr(module, '__file__', '')              if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]              if path and exists(path): files[path] = mtime(path) +          while not self.status: +            if not exists(self.lockfile)\ +            or mtime(self.lockfile) < time.time() - self.interval - 5: +                self.status = 'error' +                thread.interrupt_main()              for path, lmtime in files.iteritems():                  if not exists(path) or mtime(path) > lmtime: -                    self.status = 3 -            if not exists(self.lockfile): -                self.status = 2 -            elif mtime(self.lockfile) < time.time() - self.interval - 5: -                self.status = 1 -            if not self.status: -                time.sleep(self.interval) -        if self.status != 5: -            thread.interrupt_main() - - -def _reloader_child(server, app, interval): -    ''' Start the server and check for modified files in a background thread. -        As soon as an update is detected, KeyboardInterrupt is thrown in -        the main thread to exit the server loop. The process exists with status -        code 3 to request a reload by the observer process. If the lockfile -        is not modified in 2*interval second or missing, we assume that the -        observer process died and exit with status code 1 or 2. -    ''' -    lockfile = os.environ.get('BOTTLE_LOCKFILE') -    bgcheck = FileCheckerThread(lockfile, interval) -    try: -        bgcheck.start() -        server.run(app) -    except KeyboardInterrupt: -        pass -    bgcheck.status, status = 5, bgcheck.status -    bgcheck.join() # bgcheck.status == 5 --> silent exit -    if status: sys.exit(status) - - -def _reloader_observer(server, app, interval): -    ''' Start a child process with identical commandline arguments and restart -        it as long as it exists with status code 3. Also create a lockfile and -        touch it (update mtime) every interval seconds. -    ''' -    fd, lockfile = tempfile.mkstemp(prefix='bottle-reloader.', suffix='.lock') -    os.close(fd) # We only need this file to exist. We never write to it -    try: -        while os.path.exists(lockfile): -            args = [sys.executable] + sys.argv -            environ = os.environ.copy() -            environ['BOTTLE_CHILD'] = 'true' -            environ['BOTTLE_LOCKFILE'] = lockfile -            p = subprocess.Popen(args, env=environ) -            while p.poll() is None: # Busy wait... -                os.utime(lockfile, None) # I am alive! -                time.sleep(interval) -            if p.poll() != 3: -                if os.path.exists(lockfile): os.unlink(lockfile) -                sys.exit(p.poll()) -            elif not server.quiet: -                print "Reloading server..." -    except KeyboardInterrupt: -        pass -    if os.path.exists(lockfile): os.unlink(lockfile) - +                    self.status = 'reload' +                    thread.interrupt_main() +                    break +            time.sleep(self.interval) +     +    def __enter__(self): +        self.start() +     +    def __exit__(self, exc_type, exc_val, exc_tb): +        if not self.status: self.status = 'exit' # silent exit +        self.join() +        return issubclass(exc_type, KeyboardInterrupt) @@ -2081,7 +2451,7 @@ class TemplateError(HTTPError):  class BaseTemplate(object):      """ Base class and minimal API for template adapters """ -    extentions = ['tpl','html','thtml','stpl'] +    extensions = ['tpl','html','thtml','stpl']      settings = {} #used in prepare()      defaults = {} #used in render() @@ -2120,7 +2490,7 @@ class BaseTemplate(object):              fname = os.path.join(spath, name)              if os.path.isfile(fname):                  return fname -            for ext in cls.extentions: +            for ext in cls.extensions:                  if os.path.isfile('%s.%s' % (fname, ext)):                      return '%s.%s' % (fname, ext) @@ -2128,6 +2498,7 @@ class BaseTemplate(object):      def global_config(cls, key, *args):          ''' This reads or sets the global settings stored in class.settings. '''          if args: +            cls.settings = cls.settings.copy() # Make settings local to class              cls.settings[key] = args[0]          else:              return cls.settings[key] @@ -2185,7 +2556,7 @@ class CheetahTemplate(BaseTemplate):          self.context.vars.update(kwargs)          out = str(self.tpl)          self.context.vars.clear() -        return [out] +        return out  class Jinja2Template(BaseTemplate): @@ -2206,7 +2577,7 @@ class Jinja2Template(BaseTemplate):          for dictarg in args: kwargs.update(dictarg)          _defaults = self.defaults.copy()          _defaults.update(kwargs) -        return self.tpl.render(**_defaults).encode("utf-8") +        return self.tpl.render(**_defaults)      def loader(self, name):          fname = self.search(name, self.lookup) @@ -2228,7 +2599,6 @@ class SimpleTALTemplate(BaseTemplate):      def render(self, *args, **kwargs):          from simpletal import simpleTALES -        from StringIO import StringIO          for dictarg in args: kwargs.update(dictarg)          # TODO: maybe reuse a context instead of always creating one          context = simpleTALES.Context() @@ -2242,7 +2612,8 @@ class SimpleTALTemplate(BaseTemplate):  class SimpleTemplate(BaseTemplate): -    blocks = ('if','elif','else','try','except','finally','for','while','with','def','class') +    blocks = ('if', 'elif', 'else', 'try', 'except', 'finally', 'for', 'while', +              'with', 'def', 'class')      dedent_blocks = ('elif', 'else', 'except', 'finally')      @lazy_attribute @@ -2258,7 +2629,7 @@ class SimpleTemplate(BaseTemplate):               |\#.*                        # Comments              )''', re.VERBOSE) -    def prepare(self, escape_func=cgi.escape, noescape=False): +    def prepare(self, escape_func=html_escape, noescape=False, **kwargs):          self.cache = {}          enc = self.encoding          self._str = lambda x: touni(x, enc) @@ -2285,7 +2656,7 @@ class SimpleTemplate(BaseTemplate):          ptrbuffer = [] # Buffer for printable strings and token tuple instances          codebuffer = [] # Buffer for generated python code          multiline = dedent = oneline = False -        template = self.source if self.source else open(self.filename).read() +        template = self.source or open(self.filename, 'rb').read()          def yield_tokens(line):              for i, part in enumerate(re.split(r'\{\{(.*?)\}\}', line)): @@ -2327,7 +2698,7 @@ class SimpleTemplate(BaseTemplate):                  line = line.split('%',1)[1].lstrip() # Full line following the %                  cline = self.split_comment(line).strip()                  cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0] -                flush() ##encodig (TODO: why?) +                flush() # You are actually reading this? Good luck, it's a mess :)                  if cmd in self.blocks or multiline:                      cmd = multiline or cmd                      dedent = cmd in self.dedent_blocks # "else:" @@ -2374,15 +2745,15 @@ class SimpleTemplate(BaseTemplate):          env = self.defaults.copy()          env.update({'_stdout': _stdout, '_printlist': _stdout.extend,                 '_include': self.subtemplate, '_str': self._str, -               '_escape': self._escape}) +               '_escape': self._escape, 'get': env.get, +               'setdefault': env.setdefault, 'defined': env.__contains__})          env.update(kwargs)          eval(self.co, env)          if '_rebase' in env:              subtpl, rargs = env['_rebase'] -            subtpl = self.__class__(name=subtpl, lookup=self.lookup)              rargs['_base'] = _stdout[:] #copy stdout              del _stdout[:] # clear stdout -            return subtpl.execute(_stdout, rargs) +            return self.subtemplate(subtpl,_stdout,rargs)          return env      def render(self, *args, **kwargs): @@ -2463,11 +2834,16 @@ simpletal_view = functools.partial(view, template_adapter=SimpleTALTemplate)  TEMPLATE_PATH = ['./', './views/']  TEMPLATES = {}  DEBUG = False -MEMFILE_MAX = 1024*100 +NORUN = False # If set, run() does nothing. Used by load_app()  #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found')  HTTP_CODES = httplib.responses  HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[428] = "Precondition Required" +HTTP_CODES[429] = "Too Many Requests" +HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[511] = "Network Authentication Required" +_HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.iteritems())  #: The default template used for error pages. Override with @error()  ERROR_PAGE_TEMPLATE = """ @@ -2480,13 +2856,15 @@ ERROR_PAGE_TEMPLATE = """              <title>Error {{e.status}}: {{status_name}}</title>              <style type="text/css">                html {background-color: #eee; font-family: sans;} -              body {background-color: #fff; border: 1px solid #ddd; padding: 15px; margin: 15px;} +              body {background-color: #fff; border: 1px solid #ddd; +                    padding: 15px; margin: 15px;}                pre {background-color: #eee; border: 1px solid #ddd; padding: 5px;}              </style>          </head>          <body>              <h1>Error {{e.status}}: {{status_name}}</h1> -            <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> caused an error:</p> +            <p>Sorry, the requested URL <tt>{{repr(request.url)}}</tt> +               caused an error:</p>              <pre>{{e.output}}</pre>              %if DEBUG and e.exception:                <h2>Exception:</h2> @@ -2499,17 +2877,18 @@ ERROR_PAGE_TEMPLATE = """          </body>      </html>  %except ImportError: -    <b>ImportError:</b> Could not generate the error page. Please add bottle to sys.path +    <b>ImportError:</b> Could not generate the error page. Please add bottle to +    the import path.  %end  """ -#: A thread-save instance of :class:`Request` representing the `current` request. +#: A thread-safe instance of :class:`Request` representing the `current` request.  request = Request() -#: A thread-save instance of :class:`Response` used to build the HTTP response. +#: A thread-safe instance of :class:`Response` used to build the HTTP response.  response = Response() -#: A thread-save namepsace. Not used by Bottle. +#: A thread-safe namespace. Not used by Bottle.  local = threading.local()  # Initialize app stack (create first empty Bottle app) @@ -2520,3 +2899,28 @@ app.push()  #: A virtual package that redirects import statements.  #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`.  ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module + +if __name__ == '__main__': +    opt, args, parser = _cmd_options, _cmd_args, _cmd_parser +    if opt.version: +        print 'Bottle', __version__; sys.exit(0) +    if not args: +        parser.print_help() +        print '\nError: No application specified.\n' +        sys.exit(1) + +    try: +        sys.path.insert(0, '.') +        sys.modules.setdefault('bottle', sys.modules['__main__']) +    except (AttributeError, ImportError), e: +        parser.error(e.args[0]) + +    if opt.bind and ':' in opt.bind: +        host, port = opt.bind.rsplit(':', 1) +    else: +        host, port = (opt.bind or 'localhost'), 8080 + +    debug(opt.debug) +    run(args[0], host=host, port=port, server=opt.server, reloader=opt.reload, plugins=opt.plugin) + +# THE END diff --git a/module/remote/thriftbackend/pyload.thrift b/module/remote/thriftbackend/pyload.thrift index 5d828854c..1542e651a 100644 --- a/module/remote/thriftbackend/pyload.thrift +++ b/module/remote/thriftbackend/pyload.thrift @@ -4,6 +4,7 @@ typedef i32 FileID  typedef i32 PackageID  typedef i32 TaskID  typedef i32 ResultID +typedef i32 InteractionID  typedef list<string> LinkList  typedef string PluginName  typedef byte Progress @@ -38,6 +39,31 @@ enum ElementType {    File  } +// types for user interaction +// some may only be place holder currently not supported +// also all input - output combination are not reasonable, see InteractionManager for further info +enum Input { +  NONE, +  TEXT, +  TEXTBOX, +  PASSWORD, +  BOOL,   // confirm like, yes or no dialog +  CLICK,  // for positional captchas +  CHOICE,  // choice from list +  MULTIPLE,  // multiple choice from list of elements +  LIST, // arbitary list of elements +  TABLE  // table like data structure +} +// more can be implemented by need + +// this describes the type of the outgoing interaction +// ensure they can be logcial or'ed +enum Output { +  CAPTCHA = 1, +  QUESTION = 2, +  NOTIFICATION = 4, +} +  struct DownloadInfo {    1: FileID fid,    2: string name, @@ -111,6 +137,18 @@ struct PackageData {    13: optional list<FileID> fids  } +struct InteractionTask { +  1: InteractionID iid, +  2: Input input, +  3: list<string> structure, +  4: list<string> preset, +  5: Output output, +  6: list<string> data, +  7: string title, +  8: string description, +  9: string plugin, +} +  struct CaptchaTask {    1: i16 tid,    2: binary data, @@ -257,13 +295,6 @@ service Pyload {    list<PackageID> deleteFinished(),    void restartFailed(), -   -  //captcha -  bool isCaptchaWaiting(), -  CaptchaTask getCaptchaTask(1: bool exclusive), -  string getCaptchaTaskStatus(1: TaskID tid), -  void setCaptchaResult(1: TaskID tid, 2: string result), -      //events    list<EventInfo> getEvents(1: string uuid) @@ -289,8 +320,18 @@ service Pyload {    //info    // {plugin: {name: value}}    map<PluginName, map<string,string>> getAllInfo(), -  map<string, string> getInfoByPlugin(1: PluginName plugin) +  map<string, string> getInfoByPlugin(1: PluginName plugin),    //scheduler +  // TODO + + +  // User interaction + +  //captcha +  bool isCaptchaWaiting(), +  CaptchaTask getCaptchaTask(1: bool exclusive), +  string getCaptchaTaskStatus(1: TaskID tid), +  void setCaptchaResult(1: TaskID tid, 2: string result),  } diff --git a/module/remote/thriftbackend/thriftgen/pyload/Pyload-remote b/module/remote/thriftbackend/thriftgen/pyload/Pyload-remote index 854b1589e..bfaf5b078 100755 --- a/module/remote/thriftbackend/thriftgen/pyload/Pyload-remote +++ b/module/remote/thriftbackend/thriftgen/pyload/Pyload-remote @@ -1,6 +1,6 @@  #!/usr/bin/env python  # -# Autogenerated by Thrift Compiler (0.8.0-dev) +# Autogenerated by Thrift Compiler (0.9.0-dev)  #  # DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING  # @@ -76,10 +76,6 @@ if len(sys.argv) <= 1 or sys.argv[1] == '--help':    print '  void setPackageData(PackageID pid,  data)'    print '   deleteFinished()'    print '  void restartFailed()' -  print '  bool isCaptchaWaiting()' -  print '  CaptchaTask getCaptchaTask(bool exclusive)' -  print '  string getCaptchaTaskStatus(TaskID tid)' -  print '  void setCaptchaResult(TaskID tid, string result)'    print '   getEvents(string uuid)'    print '   getAccounts(bool refresh)'    print '   getAccountTypes()' @@ -93,6 +89,10 @@ if len(sys.argv) <= 1 or sys.argv[1] == '--help':    print '  string call(ServiceCall info)'    print '   getAllInfo()'    print '   getInfoByPlugin(PluginName plugin)' +  print '  bool isCaptchaWaiting()' +  print '  CaptchaTask getCaptchaTask(bool exclusive)' +  print '  string getCaptchaTaskStatus(TaskID tid)' +  print '  void setCaptchaResult(TaskID tid, string result)'    print ''    sys.exit(0) @@ -462,30 +462,6 @@ elif cmd == 'restartFailed':      sys.exit(1)    pp.pprint(client.restartFailed()) -elif cmd == 'isCaptchaWaiting': -  if len(args) != 0: -    print 'isCaptchaWaiting requires 0 args' -    sys.exit(1) -  pp.pprint(client.isCaptchaWaiting()) - -elif cmd == 'getCaptchaTask': -  if len(args) != 1: -    print 'getCaptchaTask requires 1 args' -    sys.exit(1) -  pp.pprint(client.getCaptchaTask(eval(args[0]),)) - -elif cmd == 'getCaptchaTaskStatus': -  if len(args) != 1: -    print 'getCaptchaTaskStatus requires 1 args' -    sys.exit(1) -  pp.pprint(client.getCaptchaTaskStatus(eval(args[0]),)) - -elif cmd == 'setCaptchaResult': -  if len(args) != 2: -    print 'setCaptchaResult requires 2 args' -    sys.exit(1) -  pp.pprint(client.setCaptchaResult(eval(args[0]),args[1],)) -  elif cmd == 'getEvents':    if len(args) != 1:      print 'getEvents requires 1 args' @@ -564,6 +540,30 @@ elif cmd == 'getInfoByPlugin':      sys.exit(1)    pp.pprint(client.getInfoByPlugin(eval(args[0]),)) +elif cmd == 'isCaptchaWaiting': +  if len(args) != 0: +    print 'isCaptchaWaiting requires 0 args' +    sys.exit(1) +  pp.pprint(client.isCaptchaWaiting()) + +elif cmd == 'getCaptchaTask': +  if len(args) != 1: +    print 'getCaptchaTask requires 1 args' +    sys.exit(1) +  pp.pprint(client.getCaptchaTask(eval(args[0]),)) + +elif cmd == 'getCaptchaTaskStatus': +  if len(args) != 1: +    print 'getCaptchaTaskStatus requires 1 args' +    sys.exit(1) +  pp.pprint(client.getCaptchaTaskStatus(eval(args[0]),)) + +elif cmd == 'setCaptchaResult': +  if len(args) != 2: +    print 'setCaptchaResult requires 2 args' +    sys.exit(1) +  pp.pprint(client.setCaptchaResult(eval(args[0]),args[1],)) +  else:    print 'Unrecognized method %s' % cmd    sys.exit(1) diff --git a/module/remote/thriftbackend/thriftgen/pyload/Pyload.py b/module/remote/thriftbackend/thriftgen/pyload/Pyload.py index a1bc63f75..78a42f16a 100644 --- a/module/remote/thriftbackend/thriftgen/pyload/Pyload.py +++ b/module/remote/thriftbackend/thriftgen/pyload/Pyload.py @@ -1,5 +1,5 @@  # -# Autogenerated by Thrift Compiler (0.8.0-dev) +# Autogenerated by Thrift Compiler (0.9.0-dev)  #  # DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING  # @@ -9,7 +9,7 @@  from thrift.Thrift import TType, TMessageType, TException  from ttypes import *  from thrift.Thrift import TProcessor -from thrift.protocol.TBase import TBase, TExceptionBase, TApplicationException +from thrift.protocol.TBase import TBase, TExceptionBase  class Iface(object): @@ -319,31 +319,6 @@ class Iface(object):    def restartFailed(self, ):      pass -  def isCaptchaWaiting(self, ): -    pass - -  def getCaptchaTask(self, exclusive): -    """ -    Parameters: -     - exclusive -    """ -    pass - -  def getCaptchaTaskStatus(self, tid): -    """ -    Parameters: -     - tid -    """ -    pass - -  def setCaptchaResult(self, tid, result): -    """ -    Parameters: -     - tid -     - result -    """ -    pass -    def getEvents(self, uuid):      """      Parameters: @@ -426,6 +401,31 @@ class Iface(object):      """      pass +  def isCaptchaWaiting(self, ): +    pass + +  def getCaptchaTask(self, exclusive): +    """ +    Parameters: +     - exclusive +    """ +    pass + +  def getCaptchaTaskStatus(self, tid): +    """ +    Parameters: +     - tid +    """ +    pass + +  def setCaptchaResult(self, tid, result): +    """ +    Parameters: +     - tid +     - result +    """ +    pass +  class Client(Iface):    def __init__(self, iprot, oprot=None): @@ -1919,121 +1919,6 @@ class Client(Iface):      self._iprot.readMessageEnd()      return -  def isCaptchaWaiting(self, ): -    self.send_isCaptchaWaiting() -    return self.recv_isCaptchaWaiting() - -  def send_isCaptchaWaiting(self, ): -    self._oprot.writeMessageBegin('isCaptchaWaiting', TMessageType.CALL, self._seqid) -    args = isCaptchaWaiting_args() -    args.write(self._oprot) -    self._oprot.writeMessageEnd() -    self._oprot.trans.flush() - -  def recv_isCaptchaWaiting(self, ): -    (fname, mtype, rseqid) = self._iprot.readMessageBegin() -    if mtype == TMessageType.EXCEPTION: -      x = TApplicationException() -      x.read(self._iprot) -      self._iprot.readMessageEnd() -      raise x -    result = isCaptchaWaiting_result() -    result.read(self._iprot) -    self._iprot.readMessageEnd() -    if result.success is not None: -      return result.success -    raise TApplicationException(TApplicationException.MISSING_RESULT, "isCaptchaWaiting failed: unknown result"); - -  def getCaptchaTask(self, exclusive): -    """ -    Parameters: -     - exclusive -    """ -    self.send_getCaptchaTask(exclusive) -    return self.recv_getCaptchaTask() - -  def send_getCaptchaTask(self, exclusive): -    self._oprot.writeMessageBegin('getCaptchaTask', TMessageType.CALL, self._seqid) -    args = getCaptchaTask_args() -    args.exclusive = exclusive -    args.write(self._oprot) -    self._oprot.writeMessageEnd() -    self._oprot.trans.flush() - -  def recv_getCaptchaTask(self, ): -    (fname, mtype, rseqid) = self._iprot.readMessageBegin() -    if mtype == TMessageType.EXCEPTION: -      x = TApplicationException() -      x.read(self._iprot) -      self._iprot.readMessageEnd() -      raise x -    result = getCaptchaTask_result() -    result.read(self._iprot) -    self._iprot.readMessageEnd() -    if result.success is not None: -      return result.success -    raise TApplicationException(TApplicationException.MISSING_RESULT, "getCaptchaTask failed: unknown result"); - -  def getCaptchaTaskStatus(self, tid): -    """ -    Parameters: -     - tid -    """ -    self.send_getCaptchaTaskStatus(tid) -    return self.recv_getCaptchaTaskStatus() - -  def send_getCaptchaTaskStatus(self, tid): -    self._oprot.writeMessageBegin('getCaptchaTaskStatus', TMessageType.CALL, self._seqid) -    args = getCaptchaTaskStatus_args() -    args.tid = tid -    args.write(self._oprot) -    self._oprot.writeMessageEnd() -    self._oprot.trans.flush() - -  def recv_getCaptchaTaskStatus(self, ): -    (fname, mtype, rseqid) = self._iprot.readMessageBegin() -    if mtype == TMessageType.EXCEPTION: -      x = TApplicationException() -      x.read(self._iprot) -      self._iprot.readMessageEnd() -      raise x -    result = getCaptchaTaskStatus_result() -    result.read(self._iprot) -    self._iprot.readMessageEnd() -    if result.success is not None: -      return result.success -    raise TApplicationException(TApplicationException.MISSING_RESULT, "getCaptchaTaskStatus failed: unknown result"); - -  def setCaptchaResult(self, tid, result): -    """ -    Parameters: -     - tid -     - result -    """ -    self.send_setCaptchaResult(tid, result) -    self.recv_setCaptchaResult() - -  def send_setCaptchaResult(self, tid, result): -    self._oprot.writeMessageBegin('setCaptchaResult', TMessageType.CALL, self._seqid) -    args = setCaptchaResult_args() -    args.tid = tid -    args.result = result -    args.write(self._oprot) -    self._oprot.writeMessageEnd() -    self._oprot.trans.flush() - -  def recv_setCaptchaResult(self, ): -    (fname, mtype, rseqid) = self._iprot.readMessageBegin() -    if mtype == TMessageType.EXCEPTION: -      x = TApplicationException() -      x.read(self._iprot) -      self._iprot.readMessageEnd() -      raise x -    result = setCaptchaResult_result() -    result.read(self._iprot) -    self._iprot.readMessageEnd() -    return -    def getEvents(self, uuid):      """      Parameters: @@ -2418,6 +2303,121 @@ class Client(Iface):        return result.success      raise TApplicationException(TApplicationException.MISSING_RESULT, "getInfoByPlugin failed: unknown result"); +  def isCaptchaWaiting(self, ): +    self.send_isCaptchaWaiting() +    return self.recv_isCaptchaWaiting() + +  def send_isCaptchaWaiting(self, ): +    self._oprot.writeMessageBegin('isCaptchaWaiting', TMessageType.CALL, self._seqid) +    args = isCaptchaWaiting_args() +    args.write(self._oprot) +    self._oprot.writeMessageEnd() +    self._oprot.trans.flush() + +  def recv_isCaptchaWaiting(self, ): +    (fname, mtype, rseqid) = self._iprot.readMessageBegin() +    if mtype == TMessageType.EXCEPTION: +      x = TApplicationException() +      x.read(self._iprot) +      self._iprot.readMessageEnd() +      raise x +    result = isCaptchaWaiting_result() +    result.read(self._iprot) +    self._iprot.readMessageEnd() +    if result.success is not None: +      return result.success +    raise TApplicationException(TApplicationException.MISSING_RESULT, "isCaptchaWaiting failed: unknown result"); + +  def getCaptchaTask(self, exclusive): +    """ +    Parameters: +     - exclusive +    """ +    self.send_getCaptchaTask(exclusive) +    return self.recv_getCaptchaTask() + +  def send_getCaptchaTask(self, exclusive): +    self._oprot.writeMessageBegin('getCaptchaTask', TMessageType.CALL, self._seqid) +    args = getCaptchaTask_args() +    args.exclusive = exclusive +    args.write(self._oprot) +    self._oprot.writeMessageEnd() +    self._oprot.trans.flush() + +  def recv_getCaptchaTask(self, ): +    (fname, mtype, rseqid) = self._iprot.readMessageBegin() +    if mtype == TMessageType.EXCEPTION: +      x = TApplicationException() +      x.read(self._iprot) +      self._iprot.readMessageEnd() +      raise x +    result = getCaptchaTask_result() +    result.read(self._iprot) +    self._iprot.readMessageEnd() +    if result.success is not None: +      return result.success +    raise TApplicationException(TApplicationException.MISSING_RESULT, "getCaptchaTask failed: unknown result"); + +  def getCaptchaTaskStatus(self, tid): +    """ +    Parameters: +     - tid +    """ +    self.send_getCaptchaTaskStatus(tid) +    return self.recv_getCaptchaTaskStatus() + +  def send_getCaptchaTaskStatus(self, tid): +    self._oprot.writeMessageBegin('getCaptchaTaskStatus', TMessageType.CALL, self._seqid) +    args = getCaptchaTaskStatus_args() +    args.tid = tid +    args.write(self._oprot) +    self._oprot.writeMessageEnd() +    self._oprot.trans.flush() + +  def recv_getCaptchaTaskStatus(self, ): +    (fname, mtype, rseqid) = self._iprot.readMessageBegin() +    if mtype == TMessageType.EXCEPTION: +      x = TApplicationException() +      x.read(self._iprot) +      self._iprot.readMessageEnd() +      raise x +    result = getCaptchaTaskStatus_result() +    result.read(self._iprot) +    self._iprot.readMessageEnd() +    if result.success is not None: +      return result.success +    raise TApplicationException(TApplicationException.MISSING_RESULT, "getCaptchaTaskStatus failed: unknown result"); + +  def setCaptchaResult(self, tid, result): +    """ +    Parameters: +     - tid +     - result +    """ +    self.send_setCaptchaResult(tid, result) +    self.recv_setCaptchaResult() + +  def send_setCaptchaResult(self, tid, result): +    self._oprot.writeMessageBegin('setCaptchaResult', TMessageType.CALL, self._seqid) +    args = setCaptchaResult_args() +    args.tid = tid +    args.result = result +    args.write(self._oprot) +    self._oprot.writeMessageEnd() +    self._oprot.trans.flush() + +  def recv_setCaptchaResult(self, ): +    (fname, mtype, rseqid) = self._iprot.readMessageBegin() +    if mtype == TMessageType.EXCEPTION: +      x = TApplicationException() +      x.read(self._iprot) +      self._iprot.readMessageEnd() +      raise x +    result = setCaptchaResult_result() +    result.read(self._iprot) +    self._iprot.readMessageEnd() +    return +  class Processor(Iface, TProcessor):    def __init__(self, handler): @@ -2476,10 +2476,6 @@ class Processor(Iface, TProcessor):      self._processMap["setPackageData"] = Processor.process_setPackageData      self._processMap["deleteFinished"] = Processor.process_deleteFinished      self._processMap["restartFailed"] = Processor.process_restartFailed -    self._processMap["isCaptchaWaiting"] = Processor.process_isCaptchaWaiting -    self._processMap["getCaptchaTask"] = Processor.process_getCaptchaTask -    self._processMap["getCaptchaTaskStatus"] = Processor.process_getCaptchaTaskStatus -    self._processMap["setCaptchaResult"] = Processor.process_setCaptchaResult      self._processMap["getEvents"] = Processor.process_getEvents      self._processMap["getAccounts"] = Processor.process_getAccounts      self._processMap["getAccountTypes"] = Processor.process_getAccountTypes @@ -2493,6 +2489,10 @@ class Processor(Iface, TProcessor):      self._processMap["call"] = Processor.process_call      self._processMap["getAllInfo"] = Processor.process_getAllInfo      self._processMap["getInfoByPlugin"] = Processor.process_getInfoByPlugin +    self._processMap["isCaptchaWaiting"] = Processor.process_isCaptchaWaiting +    self._processMap["getCaptchaTask"] = Processor.process_getCaptchaTask +    self._processMap["getCaptchaTaskStatus"] = Processor.process_getCaptchaTaskStatus +    self._processMap["setCaptchaResult"] = Processor.process_setCaptchaResult    def process(self, iprot, oprot):      (name, type, seqid) = iprot.readMessageBegin() @@ -3104,50 +3104,6 @@ class Processor(Iface, TProcessor):      oprot.writeMessageEnd()      oprot.trans.flush() -  def process_isCaptchaWaiting(self, seqid, iprot, oprot): -    args = isCaptchaWaiting_args() -    args.read(iprot) -    iprot.readMessageEnd() -    result = isCaptchaWaiting_result() -    result.success = self._handler.isCaptchaWaiting() -    oprot.writeMessageBegin("isCaptchaWaiting", TMessageType.REPLY, seqid) -    result.write(oprot) -    oprot.writeMessageEnd() -    oprot.trans.flush() - -  def process_getCaptchaTask(self, seqid, iprot, oprot): -    args = getCaptchaTask_args() -    args.read(iprot) -    iprot.readMessageEnd() -    result = getCaptchaTask_result() -    result.success = self._handler.getCaptchaTask(args.exclusive) -    oprot.writeMessageBegin("getCaptchaTask", TMessageType.REPLY, seqid) -    result.write(oprot) -    oprot.writeMessageEnd() -    oprot.trans.flush() - -  def process_getCaptchaTaskStatus(self, seqid, iprot, oprot): -    args = getCaptchaTaskStatus_args() -    args.read(iprot) -    iprot.readMessageEnd() -    result = getCaptchaTaskStatus_result() -    result.success = self._handler.getCaptchaTaskStatus(args.tid) -    oprot.writeMessageBegin("getCaptchaTaskStatus", TMessageType.REPLY, seqid) -    result.write(oprot) -    oprot.writeMessageEnd() -    oprot.trans.flush() - -  def process_setCaptchaResult(self, seqid, iprot, oprot): -    args = setCaptchaResult_args() -    args.read(iprot) -    iprot.readMessageEnd() -    result = setCaptchaResult_result() -    self._handler.setCaptchaResult(args.tid, args.result) -    oprot.writeMessageBegin("setCaptchaResult", TMessageType.REPLY, seqid) -    result.write(oprot) -    oprot.writeMessageEnd() -    oprot.trans.flush() -    def process_getEvents(self, seqid, iprot, oprot):      args = getEvents_args()      args.read(iprot) @@ -3296,6 +3252,50 @@ class Processor(Iface, TProcessor):      oprot.writeMessageEnd()      oprot.trans.flush() +  def process_isCaptchaWaiting(self, seqid, iprot, oprot): +    args = isCaptchaWaiting_args() +    args.read(iprot) +    iprot.readMessageEnd() +    result = isCaptchaWaiting_result() +    result.success = self._handler.isCaptchaWaiting() +    oprot.writeMessageBegin("isCaptchaWaiting", TMessageType.REPLY, seqid) +    result.write(oprot) +    oprot.writeMessageEnd() +    oprot.trans.flush() + +  def process_getCaptchaTask(self, seqid, iprot, oprot): +    args = getCaptchaTask_args() +    args.read(iprot) +    iprot.readMessageEnd() +    result = getCaptchaTask_result() +    result.success = self._handler.getCaptchaTask(args.exclusive) +    oprot.writeMessageBegin("getCaptchaTask", TMessageType.REPLY, seqid) +    result.write(oprot) +    oprot.writeMessageEnd() +    oprot.trans.flush() + +  def process_getCaptchaTaskStatus(self, seqid, iprot, oprot): +    args = getCaptchaTaskStatus_args() +    args.read(iprot) +    iprot.readMessageEnd() +    result = getCaptchaTaskStatus_result() +    result.success = self._handler.getCaptchaTaskStatus(args.tid) +    oprot.writeMessageBegin("getCaptchaTaskStatus", TMessageType.REPLY, seqid) +    result.write(oprot) +    oprot.writeMessageEnd() +    oprot.trans.flush() + +  def process_setCaptchaResult(self, seqid, iprot, oprot): +    args = setCaptchaResult_args() +    args.read(iprot) +    iprot.readMessageEnd() +    result = setCaptchaResult_result() +    self._handler.setCaptchaResult(args.tid, args.result) +    oprot.writeMessageBegin("setCaptchaResult", TMessageType.REPLY, seqid) +    result.write(oprot) +    oprot.writeMessageEnd() +    oprot.trans.flush() +  # HELPER FUNCTIONS AND STRUCTURES @@ -4941,139 +4941,6 @@ class restartFailed_result(TBase):    ) -class isCaptchaWaiting_args(TBase): - -  __slots__ = [  -   ] - -  thrift_spec = ( -  ) - - -class isCaptchaWaiting_result(TBase): -  """ -  Attributes: -   - success -  """ - -  __slots__ = [  -    'success', -   ] - -  thrift_spec = ( -    (0, TType.BOOL, 'success', None, None, ), # 0 -  ) - -  def __init__(self, success=None,): -    self.success = success - - -class getCaptchaTask_args(TBase): -  """ -  Attributes: -   - exclusive -  """ - -  __slots__ = [  -    'exclusive', -   ] - -  thrift_spec = ( -    None, # 0 -    (1, TType.BOOL, 'exclusive', None, None, ), # 1 -  ) - -  def __init__(self, exclusive=None,): -    self.exclusive = exclusive - - -class getCaptchaTask_result(TBase): -  """ -  Attributes: -   - success -  """ - -  __slots__ = [  -    'success', -   ] - -  thrift_spec = ( -    (0, TType.STRUCT, 'success', (CaptchaTask, CaptchaTask.thrift_spec), None, ), # 0 -  ) - -  def __init__(self, success=None,): -    self.success = success - - -class getCaptchaTaskStatus_args(TBase): -  """ -  Attributes: -   - tid -  """ - -  __slots__ = [  -    'tid', -   ] - -  thrift_spec = ( -    None, # 0 -    (1, TType.I32, 'tid', None, None, ), # 1 -  ) - -  def __init__(self, tid=None,): -    self.tid = tid - - -class getCaptchaTaskStatus_result(TBase): -  """ -  Attributes: -   - success -  """ - -  __slots__ = [  -    'success', -   ] - -  thrift_spec = ( -    (0, TType.STRING, 'success', None, None, ), # 0 -  ) - -  def __init__(self, success=None,): -    self.success = success - - -class setCaptchaResult_args(TBase): -  """ -  Attributes: -   - tid -   - result -  """ - -  __slots__ = [  -    'tid', -    'result', -   ] - -  thrift_spec = ( -    None, # 0 -    (1, TType.I32, 'tid', None, None, ), # 1 -    (2, TType.STRING, 'result', None, None, ), # 2 -  ) - -  def __init__(self, tid=None, result=None,): -    self.tid = tid -    self.result = result - - -class setCaptchaResult_result(TBase): - -  __slots__ = [  -   ] - -  thrift_spec = ( -  ) - -  class getEvents_args(TBase):    """    Attributes: @@ -5532,3 +5399,136 @@ class getInfoByPlugin_result(TBase):    def __init__(self, success=None,):      self.success = success + +class isCaptchaWaiting_args(TBase): + +  __slots__ = [  +   ] + +  thrift_spec = ( +  ) + + +class isCaptchaWaiting_result(TBase): +  """ +  Attributes: +   - success +  """ + +  __slots__ = [  +    'success', +   ] + +  thrift_spec = ( +    (0, TType.BOOL, 'success', None, None, ), # 0 +  ) + +  def __init__(self, success=None,): +    self.success = success + + +class getCaptchaTask_args(TBase): +  """ +  Attributes: +   - exclusive +  """ + +  __slots__ = [  +    'exclusive', +   ] + +  thrift_spec = ( +    None, # 0 +    (1, TType.BOOL, 'exclusive', None, None, ), # 1 +  ) + +  def __init__(self, exclusive=None,): +    self.exclusive = exclusive + + +class getCaptchaTask_result(TBase): +  """ +  Attributes: +   - success +  """ + +  __slots__ = [  +    'success', +   ] + +  thrift_spec = ( +    (0, TType.STRUCT, 'success', (CaptchaTask, CaptchaTask.thrift_spec), None, ), # 0 +  ) + +  def __init__(self, success=None,): +    self.success = success + + +class getCaptchaTaskStatus_args(TBase): +  """ +  Attributes: +   - tid +  """ + +  __slots__ = [  +    'tid', +   ] + +  thrift_spec = ( +    None, # 0 +    (1, TType.I32, 'tid', None, None, ), # 1 +  ) + +  def __init__(self, tid=None,): +    self.tid = tid + + +class getCaptchaTaskStatus_result(TBase): +  """ +  Attributes: +   - success +  """ + +  __slots__ = [  +    'success', +   ] + +  thrift_spec = ( +    (0, TType.STRING, 'success', None, None, ), # 0 +  ) + +  def __init__(self, success=None,): +    self.success = success + + +class setCaptchaResult_args(TBase): +  """ +  Attributes: +   - tid +   - result +  """ + +  __slots__ = [  +    'tid', +    'result', +   ] + +  thrift_spec = ( +    None, # 0 +    (1, TType.I32, 'tid', None, None, ), # 1 +    (2, TType.STRING, 'result', None, None, ), # 2 +  ) + +  def __init__(self, tid=None, result=None,): +    self.tid = tid +    self.result = result + + +class setCaptchaResult_result(TBase): + +  __slots__ = [  +   ] + +  thrift_spec = ( +  ) + diff --git a/module/remote/thriftbackend/thriftgen/pyload/constants.py b/module/remote/thriftbackend/thriftgen/pyload/constants.py index f5ef663f1..f8960dc63 100644 --- a/module/remote/thriftbackend/thriftgen/pyload/constants.py +++ b/module/remote/thriftbackend/thriftgen/pyload/constants.py @@ -1,5 +1,5 @@  # -# Autogenerated by Thrift Compiler (0.8.0-dev) +# Autogenerated by Thrift Compiler (0.9.0-dev)  #  # DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING  # diff --git a/module/remote/thriftbackend/thriftgen/pyload/ttypes.py b/module/remote/thriftbackend/thriftgen/pyload/ttypes.py index 626bd1c29..1299b515d 100644 --- a/module/remote/thriftbackend/thriftgen/pyload/ttypes.py +++ b/module/remote/thriftbackend/thriftgen/pyload/ttypes.py @@ -1,5 +1,5 @@  # -# Autogenerated by Thrift Compiler (0.8.0-dev) +# Autogenerated by Thrift Compiler (0.9.0-dev)  #  # DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING  # @@ -92,6 +92,61 @@ class ElementType(TBase):      "File": 1,    } +class Input(TBase): +  NONE = 0 +  TEXT = 1 +  TEXTBOX = 2 +  PASSWORD = 3 +  BOOL = 4 +  CLICK = 5 +  CHOICE = 6 +  MULTIPLE = 7 +  LIST = 8 +  TABLE = 9 + +  _VALUES_TO_NAMES = { +    0: "NONE", +    1: "TEXT", +    2: "TEXTBOX", +    3: "PASSWORD", +    4: "BOOL", +    5: "CLICK", +    6: "CHOICE", +    7: "MULTIPLE", +    8: "LIST", +    9: "TABLE", +  } + +  _NAMES_TO_VALUES = { +    "NONE": 0, +    "TEXT": 1, +    "TEXTBOX": 2, +    "PASSWORD": 3, +    "BOOL": 4, +    "CLICK": 5, +    "CHOICE": 6, +    "MULTIPLE": 7, +    "LIST": 8, +    "TABLE": 9, +  } + +class Output(TBase): +  CAPTCHA = 1 +  QUESTION = 2 +  NOTIFICATION = 4 + +  _VALUES_TO_NAMES = { +    1: "CAPTCHA", +    2: "QUESTION", +    4: "NOTIFICATION", +  } + +  _NAMES_TO_VALUES = { +    "CAPTCHA": 1, +    "QUESTION": 2, +    "NOTIFICATION": 4, +  } +  class DownloadInfo(TBase):    """ @@ -403,6 +458,57 @@ class PackageData(TBase):      self.fids = fids +class InteractionTask(TBase): +  """ +  Attributes: +   - iid +   - input +   - structure +   - preset +   - output +   - data +   - title +   - description +   - plugin +  """ + +  __slots__ = [  +    'iid', +    'input', +    'structure', +    'preset', +    'output', +    'data', +    'title', +    'description', +    'plugin', +   ] + +  thrift_spec = ( +    None, # 0 +    (1, TType.I32, 'iid', None, None, ), # 1 +    (2, TType.I32, 'input', None, None, ), # 2 +    (3, TType.LIST, 'structure', (TType.STRING,None), None, ), # 3 +    (4, TType.LIST, 'preset', (TType.STRING,None), None, ), # 4 +    (5, TType.I32, 'output', None, None, ), # 5 +    (6, TType.LIST, 'data', (TType.STRING,None), None, ), # 6 +    (7, TType.STRING, 'title', None, None, ), # 7 +    (8, TType.STRING, 'description', None, None, ), # 8 +    (9, TType.STRING, 'plugin', None, None, ), # 9 +  ) + +  def __init__(self, iid=None, input=None, structure=None, preset=None, output=None, data=None, title=None, description=None, plugin=None,): +    self.iid = iid +    self.input = input +    self.structure = structure +    self.preset = preset +    self.output = output +    self.data = data +    self.title = title +    self.description = description +    self.plugin = plugin + +  class CaptchaTask(TBase):    """    Attributes: diff --git a/module/setup.py b/module/setup.py index 4a1c59da6..85b33b1ee 100644 --- a/module/setup.py +++ b/module/setup.py @@ -17,7 +17,7 @@      @author: RaNaN  """  from getpass import getpass -import gettext +import module.common.pylgettext as gettext  import os  from os import makedirs  from os.path import abspath @@ -34,33 +34,37 @@ class Setup():      """      pyLoads initial setup configuration assistent      """ -    def __init__(self, path, config): +    def __init__(self, path, config):          self.path = path          self.config = config -      def start(self): -          langs = self.config.getMetaData("general", "language")["type"].split(";")          lang = self.ask(u"Choose your Language / Wähle deine Sprache", "en", langs) -        translation = gettext.translation("setup", join(self.path, "locale"), languages=["en", lang]) +        gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None]) +        translation = gettext.translation("setup", join(self.path, "locale"), languages=[lang, "en"], fallback=True)          translation.install(True) -#        print "" -#        print _("Would you like to configure pyLoad via Webinterface?") -#        print _("You need a Browser and a connection to this PC for it.") -#        viaweb = self.ask(_("Start initial webinterface for configuration?"), "y", bool=True) -#        if viaweb: -#            try: -#                from module.web import ServerThread -#                ServerThread.setup = self -#                from module.web import webinterface -#                webinterface.run_simple() -#                return False -#            except Exception, e: -#                print "Setup failed with this error: ", e -#                print "Falling back to commandline setup." +        #Input shorthand for yes +        self.yes = _("y") +        #Input shorthand for no +        self.no = _("n") + +        #        print "" +        #        print _("Would you like to configure pyLoad via Webinterface?") +        #        print _("You need a Browser and a connection to this PC for it.") +        #        viaweb = self.ask(_("Start initial webinterface for configuration?"), "y", bool=True) +        #        if viaweb: +        #            try: +        #                from module.web import ServerThread +        #                ServerThread.setup = self +        #                from module.web import webinterface +        #                webinterface.run_simple() +        #                return False +        #            except Exception, e: +        #                print "Setup failed with this error: ", e +        #                print "Falling back to commandline setup."          print "" @@ -69,13 +73,14 @@ class Setup():          print ""          print _("The value in brackets [] always is the default value,")          print _("in case you don't want to change it or you are unsure what to choose, just hit enter.") -        print _("Don't forget: You can always rerun this assistent with --setup or -s parameter, when you start pyLoadCore.") +        print _( +            "Don't forget: You can always rerun this assistent with --setup or -s parameter, when you start pyLoadCore.")          print _("If you have any problems with this assistent hit STRG-C,")          print _("to abort and don't let him start with pyLoadCore automatically anymore.")          print ""          print _("When you are ready for system check, hit enter.")          raw_input() -         +          basic, ssl, captcha, gui, web, js = self.system_check()          print "" @@ -90,8 +95,7 @@ class Setup():          print ""          print _("## Status ##")          print "" -         -         +          avail = []          if self.check_module("Crypto"): avail.append(_("container decrypting"))          if ssl: avail.append(_("ssl connection")) @@ -99,64 +103,64 @@ class Setup():          if gui: avail.append(_("GUI"))          if web: avail.append(_("Webinterface"))          if js: avail.append(_("extended Click'N'Load")) -  +          string = "" -         +          for av in avail: -            string += ", "+av +            string += ", " + av          print _("Features available:") + string[1:]          print "" -         +          if len(avail) < 5:              print _("Featues missing: ")              print -             +              if not self.check_module("Crypto"):                  print _("no py-crypto available")                  print _("You need this if you want to decrypt container files.")                  print "" -             +              if not ssl:                  print _("no SSL available")                  print _("This is needed if you want to establish a secure connection to core or webinterface.")                  print _("If you only want to access locally to pyLoad ssl is not usefull.")                  print "" -             +              if not captcha:                  print _("no Captcha Recognition available")                  print _("Only needed for some hosters and as freeuser.")                  print "" -             +              if not gui:                  print _("Gui not available")                  print _("The Graphical User Interface.")                  print "" -                 +              if not js:                  print _("no JavaScript engine found")                  print _("You will need this for some Click'N'Load links. Install Spidermonkey, ossp-js, pyv8 or rhino") -             +              print _("You can abort the setup now and fix some dependicies if you want.") -        con = self.ask(_("Continue with setup?"), "y", bool=True) +        con = self.ask(_("Continue with setup?"), self.yes, bool=True)          if not con:              return False          print "" -        print _("Do you want to change the config path? Current is %s" % abspath("")) -        print _("If you use pyLoad on a server or the home partition lives on an iternal flash it may be a good idea to change it.") -        path = self.ask(_("Change config path?"), "n", bool=True) +        print _("Do you want to change the config path? Current is %s") % abspath("") +        print _( +            "If you use pyLoad on a server or the home partition lives on an iternal flash it may be a good idea to change it.") +        path = self.ask(_("Change config path?"), self.no, bool=True)          if path:              self.conf_path()              #calls exit when changed -         -         +          print ""          print _("Do you want to configure login data and basic settings?")          print _("This is recommend for first run.") -        con = self.ask(_("Make basic setup?"), "y", bool=True) +        con = self.ask(_("Make basic setup?"), self.yes, bool=True)          if con:              self.conf_basic() @@ -164,14 +168,14 @@ class Setup():          if ssl:              print ""              print _("Do you want to configure ssl?") -            ssl = self.ask(_("Configure ssl?"), "n", bool=True) +            ssl = self.ask(_("Configure ssl?"), self.no, bool=True)              if ssl:                  self.conf_ssl()          if web:              print ""              print _("Do you want to configure webinterface?") -            web = self.ask(_("Configure webinterface?"), "y", bool=True) +            web = self.ask(_("Configure webinterface?"), self.yes, bool=True)              if web:                  self.conf_web() @@ -197,7 +201,6 @@ class Setup():              print _("Python Version: OK")              python = True -          curl = self.check_module("pycurl")          self.print_dep("pycurl", curl) @@ -207,10 +210,10 @@ class Setup():          basic = python and curl and sqlite          print "" -         +          crypto = self.check_module("Crypto")          self.print_dep("pycrypto", crypto) -         +          ssl = self.check_module("OpenSSL")          self.print_dep("py-OpenSSL", ssl) @@ -218,12 +221,12 @@ class Setup():          pil = self.check_module("Image")          self.print_dep("py-imaging", pil) -         +          if os.name == "nt":              tesser = self.check_prog([join(pypath, "tesseract", "tesseract.exe"), "-v"])          else:              tesser = self.check_prog(["tesseract", "-v"]) -         +          self.print_dep("tesseract", tesser)          captcha = pil and tesser @@ -238,6 +241,7 @@ class Setup():          try:              import jinja2 +              v = jinja2.__version__              if v and "unknown" not in v:                  if not v.startswith("2.5") and not v.startswith("2.6"): @@ -246,15 +250,13 @@ class Setup():                      print _("please upgrade or deinstall it, pyLoad includes a sufficient jinja2 libary.")                      print                      jinja = False -        except : +        except:              pass -          self.print_dep("jinja2", jinja)          beaker = self.check_module("beaker")          self.print_dep("beaker", beaker) -          web = sqlite and beaker          from module.common import JsEngine @@ -270,11 +272,12 @@ class Setup():          print ""          print _("The following logindata is valid for CLI, GUI and webinterface.") -         +          from module.database import DatabaseBackend +          db = DatabaseBackend(None)          db.setup() -        username = self.ask(_("Username"), "User")        +        username = self.ask(_("Username"), "User")          password = self.ask("", "", password=True)          db.addUser(username, password)          db.shutdown() @@ -282,20 +285,18 @@ class Setup():          print ""          print _("External clients (GUI, CLI or other) need remote access to work over the network.")          print _("However, if you only want to use the webinterface you may disable it to save ram.") -        self.config["remote"]["activated"] = self.ask(_("Enable remote access"), "y", bool=True) - +        self.config["remote"]["activated"] = self.ask(_("Enable remote access"), self.yes, bool=True)          print ""          langs = self.config.getMetaData("general", "language")          self.config["general"]["language"] = self.ask(_("Language"), "en", langs["type"].split(";")) -                  self.config["general"]["download_folder"] = self.ask(_("Downloadfolder"), "Downloads")          self.config["download"]["max_downloads"] = self.ask(_("Max parallel downloads"), "3")          #print _("You should disable checksum proofing, if you have low hardware requirements.")          #self.config["general"]["checksum"] = self.ask(_("Proof checksum?"), "y", bool=True) -        reconnect = self.ask(_("Use Reconnect?"), "n", bool=True) +        reconnect = self.ask(_("Use Reconnect?"), self.no, bool=True)          self.config["reconnect"]["activated"] = reconnect          if reconnect:              self.config["reconnect"]["method"] = self.ask(_("Reconnect script location"), "./reconnect.sh") @@ -304,9 +305,9 @@ class Setup():      def conf_web(self):          print ""          print _("## Webinterface Setup ##") -   +          print "" -        self.config["webinterface"]["activated"] = self.ask(_("Activate webinterface?"), "y", bool=True) +        self.config["webinterface"]["activated"] = self.ask(_("Activate webinterface?"), self.yes, bool=True)          print ""          print _("Listen address, if you use 127.0.0.1 or localhost, the webinterface will only accessible locally.")          self.config["webinterface"]["host"] = self.ask(_("Address"), "0.0.0.0") @@ -315,17 +316,19 @@ class Setup():          print _("pyLoad offers several server backends, now following a short explanation.")          print "builtin:", _("Default server, best choice if you dont know which one to choose.")          print "threaded:", _("This server offers SSL and is a good alternative to builtin.") -        print "fastcgi:", _("Can be used by apache, lighttpd, requires you to configure them, which is not too easy job.") +        print "fastcgi:", _( +            "Can be used by apache, lighttpd, requires you to configure them, which is not too easy job.")          print "lightweight:", _("Very fast alternative written in C, requires libev and linux knowlegde.")          print "\t", _("Get it from here: https://github.com/jonashaag/bjoern, compile it")          print "\t", _("and copy bjoern.so to module/lib")          print -        print _("Attention: In some rare cases the builtin server is not working, if you notice problems with the webinterface") +        print _( +            "Attention: In some rare cases the builtin server is not working, if you notice problems with the webinterface")          print _("come back here and change the builtin server to the threaded one here.") - -        self.config["webinterface"]["server"] = self.ask(_("Server"), "builtin", ["builtin", "threaded", "fastcgi", "lightweight"]) +        self.config["webinterface"]["server"] = self.ask(_("Server"), "builtin", +            ["builtin", "threaded", "fastcgi", "lightweight"])      def conf_ssl(self):          print "" @@ -339,17 +342,19 @@ class Setup():          print ""          print _("If you're done and everything went fine, you can activate ssl now.") -        self.config["ssl"]["activated"] = self.ask(_("Activate SSL?"), "y", bool=True) +        self.config["ssl"]["activated"] = self.ask(_("Activate SSL?"), self.yes, bool=True)      def set_user(self): - -        translation = gettext.translation("setup", join(self.path, "locale"), languages=["en", self.config["general"]["language"]]) +        gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None]) +        translation = gettext.translation("setup", join(self.path, "locale"), +            languages=[self.config["general"]["language"], "en"], fallback=True)          translation.install(True) -         +          from module.database import DatabaseBackend +          db = DatabaseBackend(None)          db.setup() -         +          noaction = True          try:              while True: @@ -363,7 +368,7 @@ class Setup():                      continue                  elif action == "1":                      print "" -                    username = self.ask(_("Username"), "User")        +                    username = self.ask(_("Username"), "User")                      password = self.ask("", "", password=True)                      db.addUser(username, password)                      noaction = False @@ -394,16 +399,18 @@ class Setup():      def conf_path(self, trans=False):          if trans: -            translation = gettext.translation("setup", join(self.path, "locale"), languages=[self.config["general"]["language"]]) +            gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None]) +            translation = gettext.translation("setup", join(self.path, "locale"), +                languages=[self.config["general"]["language"], "en"], fallback=True)              translation.install(True) -         +          print _("Setting new configpath, current configuration will not be transfered!")          path = self.ask(_("Configpath"), abspath(""))          try:              path = join(pypath, path)              if not exists(path):                  makedirs(path) -            f = open(join(pypath, "module","config", "configdir"), "wb") +            f = open(join(pypath, "module", "config", "configdir"), "wb")              f.write(path)              f.close()              print _("Configpath changed, setup will now close, please restart to go on.") @@ -412,7 +419,7 @@ class Setup():              exit()          except Exception, e:              print _("Setting config path failed: %s") % str(e) -         +      def print_dep(self, name, value):          """Print Status of dependency"""          if value: @@ -446,10 +453,10 @@ class Setup():              info += ")"          elif bool: -            if default == "y": -                info = "([y]/n)" +            if default == self.yes: +                info = _("[y]/n")              else: -                info = "(y/[n])" +                info = _("y/[n]")          else:              info = "[%s]" % default @@ -457,7 +464,6 @@ class Setup():              p1 = True              p2 = False              while p1 != p2: -                  if os.name == "nt":                      qst = str("Password: ") #no unicode on windows                  else: @@ -489,15 +495,16 @@ class Setup():                  input = default              if bool: -                if re.match(r"(y|yes|j|ja|true)", input.lower().strip()): +                # yes, true,t are inputs for booleans with value true +                if input.lower().strip() in [self.yes, _("yes"), _("true"), _("t")]:                      return True -                elif re.match(r"(n|no|nein|false)", input.lower().strip()): +                # no, false,f are inputs for booleans with value false +                elif input.lower().strip() in [self.no, _("no"), _("false"), _("f")]:                      return False                  else:                      print _("Invalid Input")                      continue -              if not answers:                  return input diff --git a/module/web/pyload_app.py b/module/web/pyload_app.py index e6c9ab436..9612e495e 100644 --- a/module/web/pyload_app.py +++ b/module/web/pyload_app.py @@ -381,7 +381,7 @@ def path(file="", path=""):  @route("/logs", method="POST")  @route("/logs/:item")  @route("/logs/:item", method="POST") -@login_required('STATUS') +@login_required('LOGS')  def logs(item=-1):      s = request.environ.get('beaker.session') diff --git a/module/web/webinterface.py b/module/web/webinterface.py index 68724e3f6..ec8b2e56c 100644 --- a/module/web/webinterface.py +++ b/module/web/webinterface.py @@ -18,8 +18,9 @@  """  import sys -import gettext +import module.common.pylgettext as gettext +import os  from os.path import join, abspath, dirname, exists  from os import makedirs @@ -98,8 +99,9 @@ if PREFIX:  else:      env.filters["url"] = lambda x: PREFIX + x if x.startswith("/") else x +gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None])  translation = gettext.translation("django", join(PYLOAD_DIR, "locale"), -    languages=["en", config.get("general", "language")]) +    languages=[config.get("general", "language"), "en"],fallback=True)  translation.install(True)  env.install_gettext_translations(translation) diff --git a/pavement.py b/pavement.py index 852179e94..7f956e78d 100644 --- a/pavement.py +++ b/pavement.py @@ -37,13 +37,12 @@ setup(      #data_files=[],      include_package_data=True,      exclude_package_data={'pyload': ['docs*', 'scripts*']}, #exluced from build but not from sdist -    #leaving out thrift 0.8.0 since its not statisfiable -    install_requires=['BeautifulSoup>=3.2, <3.3', 'jinja2', 'pycurl', 'Beaker', 'bottle >= 0.9.0'] + extradeps, +    install_requires=['thrift >= 0.8.0', 'jinja2', 'pycurl', 'Beaker', 'bottle >= 0.10.0', 'BeautifulSoup>=3.2, <3.3'] + extradeps,      extras_require={          'SSL': ["pyOpenSSL"],          'DLC': ['pycrypto'],          'lightweight webserver': ['bjoern'], -        'RSS plugins': ['feedparser'] +        'RSS plugins': ['feedparser'],      },      #setup_requires=["setuptools_hg"],      entry_points={ diff --git a/pyLoadCli.py b/pyLoadCli.py index c992914a7..079cee19c 100755 --- a/pyLoadCli.py +++ b/pyLoadCli.py @@ -20,7 +20,7 @@  from __future__ import with_statement  from getopt import GetoptError, getopt -import gettext +import module.common.pylgettext as gettext  import os  from os import _exit  from os.path import join, exists, abspath, basename @@ -490,8 +490,9 @@ def main():          for opt in configFile.items("cli"):              config[opt[0]] = opt[1] +    gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None])      translation = gettext.translation("pyLoadCli", join(pypath, "locale"), -        languages=["en", config["language"]]) +        languages=[config["language"],"en"],fallback=True)      translation.install(unicode=True)      interactive = False @@ -515,8 +516,9 @@ def main():                  config["port"] = params              elif option in ("-l", "--language"):                  config["language"] = params +                gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None])                  translation = gettext.translation("pyLoadCli", join(pypath, "locale"), -                    languages=["en", config["language"]]) +                    languages=[config["language"],"en"],fallback=True)                  translation.install(unicode=True)              elif option in ("-h", "--help"):                  print_help(config) @@ -585,4 +587,4 @@ def main():  if __name__ == "__main__": -    main()
\ No newline at end of file +    main() diff --git a/pyLoadCore.py b/pyLoadCore.py index cbc270036..99d8642bd 100755 --- a/pyLoadCore.py +++ b/pyLoadCore.py @@ -25,7 +25,7 @@ CURRENT_VERSION = '0.4.8'  import __builtin__  from getopt import getopt, GetoptError -import gettext +import module.common.pylgettext as gettext  from imp import find_module  import logging  import logging.handlers @@ -288,8 +288,9 @@ class Core(object):          self.config = ConfigParser() +        gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None])          translation = gettext.translation("pyLoad", self.path("locale"), -                                          languages=["en", self.config['general']['language']]) +                                          languages=[self.config['general']['language'],"en"],fallback=True)          translation.install(True)          self.debug = self.doDebug or self.config['general']['debug_mode'] diff --git a/pyLoadGui.py b/pyLoadGui.py index b8dd6995d..5f620e52a 100755 --- a/pyLoadGui.py +++ b/pyLoadGui.py @@ -29,7 +29,8 @@ from PyQt4.QtCore import *  from PyQt4.QtGui import *  import re -import gettext +import module.common.pylgettext as gettext +import os  from os.path import abspath  from os.path import join  from os.path import basename @@ -77,7 +78,8 @@ class main(QObject):              parser = XMLParser(join(self.path, "module", "config", "gui_default.xml"))              lang = parser.xml.elementsByTagName("language").item(0).toElement().text() -        translation = gettext.translation("pyLoadGui", join(pypath, "locale"), languages=["en", str(lang)]) +        gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None]) +        translation = gettext.translation("pyLoadGui", join(pypath, "locale"), languages=[str(lang), "en"], fallback=True)          try:              translation.install(unicode=(True if sys.stdout.encoding.lower().startswith("utf") else False))          except: | 
