From cc797f7970dc13a3aef9dcee4678094abc0a0b25 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Wed, 7 Dec 2011 19:05:33 +0100 Subject: new l18n files, closed #448 --- locale/cli.pot | 116 ++++++++++---------- locale/core.pot | 315 ++++++++++++++++++++++++++---------------------------- locale/django.pot | 123 ++++++++++++--------- locale/gui.pot | 10 +- locale/setup.pot | 116 +++++++++++--------- 5 files changed, 355 insertions(+), 325 deletions(-) diff --git a/locale/cli.pot b/locale/cli.pot index b2d4f50a1..13ff3e5c2 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:00+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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 ..." 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:546 msgid "You need py-openssl to connect to this pyLoad Core." msgstr "" -#: pyLoadCli.py:556 +#: pyLoadCli.py:553 msgid "Address: " msgstr "" -#: pyLoadCli.py:557 +#: pyLoadCli.py:554 msgid "Port: " msgstr "" -#: pyLoadCli.py:558 +#: pyLoadCli.py:555 msgid "Username: " msgstr "" -#: pyLoadCli.py:562 +#: pyLoadCli.py:559 msgid "Password: " msgstr "" -#: pyLoadCli.py:567 pyLoadCli.py:576 +#: pyLoadCli.py:564 pyLoadCli.py:573 msgid "Login data is wrong." msgstr "" -#: pyLoadCli.py:569 pyLoadCli.py:578 +#: pyLoadCli.py:566 pyLoadCli.py:575 #, python-format msgid "Could not establish connection to %(addr)s:%(port)s." msgstr "" -#: pyLoadCli.py:581 +#: pyLoadCli.py:578 msgid "You need py-openssl to connect to this pyLoad core." msgstr "" -#: pyLoadCli.py:583 +#: pyLoadCli.py:580 msgid "Interactive mode ignored since you passed some commands." msgstr "" diff --git a/locale/core.pot b/locale/core.pot index 6b38ef5e8..0cdc66476 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:00+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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:300 #, python-format msgid "pyLoad already running with pid %s" msgstr "" -#: pyLoadCore.py:302 +#: pyLoadCore.py:314 #, python-format msgid "Failed changing group: %s" msgstr "" -#: pyLoadCore.py:312 +#: pyLoadCore.py:324 #, python-format msgid "Failed changing user: %s" msgstr "" -#: pyLoadCore.py:314 +#: pyLoadCore.py:326 msgid "folder for logs" msgstr "" -#: pyLoadCore.py:325 +#: pyLoadCore.py:337 msgid "Starting" msgstr "" -#: pyLoadCore.py:326 +#: pyLoadCore.py:338 #, python-format msgid "Using home directory: %s" msgstr "" -#: pyLoadCore.py:332 +#: pyLoadCore.py:347 msgid "pycrypto to decode container files" msgstr "" -#: pyLoadCore.py:335 +#: pyLoadCore.py:350 msgid "folder for temporary files" msgstr "" -#: pyLoadCore.py:340 +#: pyLoadCore.py:355 msgid "folder for downloads" msgstr "" -#: pyLoadCore.py:343 +#: pyLoadCore.py:358 msgid "OpenSSL for secure connection" msgstr "" -#: pyLoadCore.py:347 +#: pyLoadCore.py:362 msgid "Moving old user config to DB" msgstr "" -#: pyLoadCore.py:350 +#: pyLoadCore.py:365 msgid "Please check your logindata with ./pyLoadCore.py -u" msgstr "" -#: pyLoadCore.py:353 +#: pyLoadCore.py:368 msgid "All links removed" msgstr "" -#: pyLoadCore.py:378 +#: pyLoadCore.py:399 #, python-format msgid "Downloadtime: %s" msgstr "" -#: pyLoadCore.py:390 +#: pyLoadCore.py:409 #, python-format msgid "Free space: %s" msgstr "" -#: pyLoadCore.py:410 +#: pyLoadCore.py:429 msgid "Activating Accounts..." msgstr "" -#: pyLoadCore.py:416 +#: pyLoadCore.py:435 msgid "Activating Plugins..." msgstr "" -#: pyLoadCore.py:419 +#: pyLoadCore.py:438 msgid "pyLoad is up and running" msgstr "" -#: pyLoadCore.py:438 +#: pyLoadCore.py:457 msgid "restarting pyLoad" msgstr "" -#: pyLoadCore.py:442 +#: pyLoadCore.py:461 msgid "pyLoad quits" msgstr "" -#: pyLoadCore.py:499 +#: pyLoadCore.py:518 #, python-format msgid "Install %s" msgstr "" -#: pyLoadCore.py:535 +#: pyLoadCore.py:554 #, python-format msgid "could not find %(desc)s: %(name)s" msgstr "" -#: pyLoadCore.py:537 +#: pyLoadCore.py:556 #, python-format msgid "could not create %(desc)s: %(name)s" msgstr "" -#: pyLoadCore.py:551 +#: pyLoadCore.py:577 msgid "shutting down..." msgstr "" -#: pyLoadCore.py:568 +#: pyLoadCore.py:594 msgid "error while shutting down" msgstr "" -#: pyLoadCore.py:634 +#: pyLoadCore.py:658 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..ba34f70c5 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:00+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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..f8a56e738 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:00+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -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..fdf465201 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:00+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -229,211 +229,227 @@ msgstr "" msgid "Python Version: OK" msgstr "" -#: module/setup.py:242 +#: module/setup.py:244 #, python-format msgid "Your installed jinja2 version %s seems too old." msgstr "" -#: module/setup.py:243 +#: module/setup.py:245 msgid "You can safely continue but if the webinterface is not working," msgstr "" -#: module/setup.py:244 +#: module/setup.py:246 msgid "" "please upgrade or deinstall it, pyLoad includes a sufficient jinja2 libary." msgstr "" -#: module/setup.py:261 +#: module/setup.py:263 msgid "JS engine" msgstr "" -#: module/setup.py:267 +#: module/setup.py:269 msgid "## Basic Setup ##" msgstr "" -#: module/setup.py:270 +#: module/setup.py:272 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:277 module/setup.py:366 module/setup.py:382 msgid "Username" msgstr "" -#: module/setup.py:282 -msgid "Language" +#: module/setup.py:283 +msgid "" +"External clients (GUI, CLI or other) need remote access to work over the " +"network." +msgstr "" + +#: module/setup.py:284 +msgid "" +"However, if you only want to use the webinterface you may disable it to save " +"ram." msgstr "" #: module/setup.py:285 +msgid "Enable remote access" +msgstr "" + +#: module/setup.py:290 +msgid "Language" +msgstr "" + +#: module/setup.py:293 msgid "Downloadfolder" msgstr "" -#: module/setup.py:286 +#: module/setup.py:294 msgid "Max parallel downloads" msgstr "" -#: module/setup.py:290 +#: module/setup.py:298 msgid "Use Reconnect?" msgstr "" -#: module/setup.py:293 +#: module/setup.py:301 msgid "Reconnect script location" msgstr "" -#: module/setup.py:298 +#: module/setup.py:306 msgid "## Webinterface Setup ##" msgstr "" -#: module/setup.py:301 +#: module/setup.py:309 msgid "Activate webinterface?" msgstr "" -#: module/setup.py:303 +#: module/setup.py:311 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:312 msgid "Address" msgstr "" -#: module/setup.py:305 +#: module/setup.py:313 msgid "Port" msgstr "" -#: module/setup.py:307 +#: module/setup.py:315 msgid "" "pyLoad offers several server backends, now following a short explanation." msgstr "" -#: module/setup.py:308 +#: module/setup.py:316 msgid "Default server, best choice if you dont know which one to choose." msgstr "" -#: module/setup.py:309 +#: module/setup.py:317 msgid "This server offers SSL and is a good alternative to builtin." msgstr "" -#: module/setup.py:310 +#: module/setup.py:318 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:319 msgid "Very fast alternative written in C, requires libev and linux knowlegde." msgstr "" -#: module/setup.py:312 +#: module/setup.py:320 msgid "Get it from here: https://github.com/jonashaag/bjoern, compile it" msgstr "" -#: module/setup.py:313 +#: module/setup.py:321 msgid "and copy bjoern.so to module/lib" msgstr "" -#: module/setup.py:316 +#: module/setup.py:324 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:325 msgid "come back here and change the builtin server to the threaded one here." msgstr "" -#: module/setup.py:320 +#: module/setup.py:328 msgid "Server" msgstr "" -#: module/setup.py:324 +#: module/setup.py:332 msgid "## SSL Setup ##" msgstr "" -#: module/setup.py:326 +#: module/setup.py:334 msgid "" "Execute these commands from pyLoad config folder to make ssl certificates:" msgstr "" -#: module/setup.py:332 +#: module/setup.py:340 msgid "If you're done and everything went fine, you can activate ssl now." msgstr "" -#: module/setup.py:334 +#: module/setup.py:342 msgid "Activate SSL?" msgstr "" -#: module/setup.py:348 +#: module/setup.py:356 msgid "Select action" msgstr "" -#: module/setup.py:349 +#: module/setup.py:357 msgid "1 - Create/Edit user" msgstr "" -#: module/setup.py:350 +#: module/setup.py:358 msgid "2 - List users" msgstr "" -#: module/setup.py:351 +#: module/setup.py:359 msgid "3 - Remove user" msgstr "" -#: module/setup.py:352 +#: module/setup.py:360 msgid "4 - Quit" msgstr "" -#: module/setup.py:364 +#: module/setup.py:372 msgid "Users" msgstr "" -#: module/setup.py:392 +#: module/setup.py:400 msgid "Setting new configpath, current configuration will not be transfered!" msgstr "" -#: module/setup.py:393 +#: module/setup.py:401 msgid "Configpath" msgstr "" -#: module/setup.py:401 +#: module/setup.py:409 msgid "Configpath changed, setup will now close, please restart to go on." msgstr "" -#: module/setup.py:402 +#: module/setup.py:410 msgid "Press Enter to exit." msgstr "" -#: module/setup.py:406 +#: module/setup.py:414 #, python-format msgid "Setting config path failed: %s" msgstr "" -#: module/setup.py:411 +#: module/setup.py:419 #, python-format msgid "%s: OK" msgstr "" -#: module/setup.py:413 +#: module/setup.py:421 #, python-format msgid "%s: missing" msgstr "" -#: module/setup.py:456 +#: module/setup.py:464 msgid "Password: " msgstr "" -#: module/setup.py:461 +#: module/setup.py:469 msgid "Password to short. Use at least 4 symbols." msgstr "" -#: module/setup.py:467 +#: module/setup.py:475 msgid "Password (again): " msgstr "" -#: module/setup.py:474 +#: module/setup.py:482 msgid "Passwords did not match." msgstr "" -#: module/setup.py:489 module/setup.py:500 +#: module/setup.py:497 module/setup.py:508 msgid "Invalid Input" msgstr "" -- cgit v1.2.3 From b575e03d6621cd236df7de3879507efa38ad16b8 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Wed, 7 Dec 2011 19:22:02 +0100 Subject: closed #436 --- locale/cli.pot | 20 ++-- locale/core.pot | 54 +++++----- locale/django.pot | 2 +- locale/gui.pot | 22 ++-- locale/setup.pot | 246 +++++++++++++++++++++++++------------------- module/common/pylgettext.py | 62 +++++++++++ module/setup.py | 52 ++++++---- module/web/webinterface.py | 6 +- pyLoadCli.py | 10 +- pyLoadCore.py | 5 +- pyLoadGui.py | 6 +- 11 files changed, 301 insertions(+), 184 deletions(-) create mode 100644 module/common/pylgettext.py diff --git a/locale/cli.pot b/locale/cli.pot index 13ff3e5c2..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-12-07 19:00+0100\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 \n" "Language-Team: LANGUAGE \n" @@ -187,40 +187,40 @@ msgstr "" msgid "Couldn't write user config file" msgstr "" -#: pyLoadCli.py:546 +#: pyLoadCli.py:548 msgid "You need py-openssl to connect to this pyLoad Core." msgstr "" -#: pyLoadCli.py:553 +#: pyLoadCli.py:555 msgid "Address: " msgstr "" -#: pyLoadCli.py:554 +#: pyLoadCli.py:556 msgid "Port: " msgstr "" -#: pyLoadCli.py:555 +#: pyLoadCli.py:557 msgid "Username: " msgstr "" -#: pyLoadCli.py:559 +#: pyLoadCli.py:561 msgid "Password: " msgstr "" -#: pyLoadCli.py:564 pyLoadCli.py:573 +#: pyLoadCli.py:566 pyLoadCli.py:575 msgid "Login data is wrong." msgstr "" -#: pyLoadCli.py:566 pyLoadCli.py:575 +#: pyLoadCli.py:568 pyLoadCli.py:577 #, python-format msgid "Could not establish connection to %(addr)s:%(port)s." msgstr "" -#: pyLoadCli.py:578 +#: pyLoadCli.py:580 msgid "You need py-openssl to connect to this pyLoad core." msgstr "" -#: pyLoadCli.py:580 +#: pyLoadCli.py:582 msgid "Interactive mode ignored since you passed some commands." msgstr "" diff --git a/locale/core.pot b/locale/core.pot index 0cdc66476..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-12-07 19:00+0100\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 \n" "Language-Team: LANGUAGE \n" @@ -21,116 +21,116 @@ msgstr "" msgid "Received Quit signal" msgstr "" -#: pyLoadCore.py:300 +#: pyLoadCore.py:301 #, python-format msgid "pyLoad already running with pid %s" msgstr "" -#: pyLoadCore.py:314 +#: pyLoadCore.py:315 #, python-format msgid "Failed changing group: %s" msgstr "" -#: pyLoadCore.py:324 +#: pyLoadCore.py:325 #, python-format msgid "Failed changing user: %s" msgstr "" -#: pyLoadCore.py:326 +#: pyLoadCore.py:327 msgid "folder for logs" msgstr "" -#: pyLoadCore.py:337 +#: pyLoadCore.py:338 msgid "Starting" msgstr "" -#: pyLoadCore.py:338 +#: pyLoadCore.py:339 #, python-format msgid "Using home directory: %s" msgstr "" -#: pyLoadCore.py:347 +#: pyLoadCore.py:348 msgid "pycrypto to decode container files" msgstr "" -#: pyLoadCore.py:350 +#: pyLoadCore.py:351 msgid "folder for temporary files" msgstr "" -#: pyLoadCore.py:355 +#: pyLoadCore.py:356 msgid "folder for downloads" msgstr "" -#: pyLoadCore.py:358 +#: pyLoadCore.py:359 msgid "OpenSSL for secure connection" msgstr "" -#: pyLoadCore.py:362 +#: pyLoadCore.py:363 msgid "Moving old user config to DB" msgstr "" -#: pyLoadCore.py:365 +#: pyLoadCore.py:366 msgid "Please check your logindata with ./pyLoadCore.py -u" msgstr "" -#: pyLoadCore.py:368 +#: pyLoadCore.py:369 msgid "All links removed" msgstr "" -#: pyLoadCore.py:399 +#: pyLoadCore.py:400 #, python-format msgid "Downloadtime: %s" msgstr "" -#: pyLoadCore.py:409 +#: pyLoadCore.py:410 #, python-format msgid "Free space: %s" msgstr "" -#: pyLoadCore.py:429 +#: pyLoadCore.py:430 msgid "Activating Accounts..." msgstr "" -#: pyLoadCore.py:435 +#: pyLoadCore.py:436 msgid "Activating Plugins..." msgstr "" -#: pyLoadCore.py:438 +#: pyLoadCore.py:439 msgid "pyLoad is up and running" msgstr "" -#: pyLoadCore.py:457 +#: pyLoadCore.py:458 msgid "restarting pyLoad" msgstr "" -#: pyLoadCore.py:461 +#: pyLoadCore.py:462 msgid "pyLoad quits" msgstr "" -#: pyLoadCore.py:518 +#: pyLoadCore.py:519 #, python-format msgid "Install %s" msgstr "" -#: pyLoadCore.py:554 +#: pyLoadCore.py:555 #, python-format msgid "could not find %(desc)s: %(name)s" msgstr "" -#: pyLoadCore.py:556 +#: pyLoadCore.py:557 #, python-format msgid "could not create %(desc)s: %(name)s" msgstr "" -#: pyLoadCore.py:577 +#: pyLoadCore.py:578 msgid "shutting down..." msgstr "" -#: pyLoadCore.py:594 +#: pyLoadCore.py:595 msgid "error while shutting down" msgstr "" -#: pyLoadCore.py:658 +#: pyLoadCore.py:659 msgid "killed pyLoad from Terminal" msgstr "" diff --git a/locale/django.pot b/locale/django.pot index ba34f70c5..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-12-07 19:00+0100\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 \n" "Language-Team: LANGUAGE \n" diff --git a/locale/gui.pot b/locale/gui.pot index f8a56e738..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-12-07 19:00+0100\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 \n" "Language-Team: LANGUAGE \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 "" diff --git a/locale/setup.pot b/locale/setup.pot index fdf465201..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-12-07 19:00+0100\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 \n" "Language-Team: LANGUAGE \n" @@ -17,439 +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:244 +#: module/setup.py:249 #, python-format msgid "Your installed jinja2 version %s seems too old." msgstr "" -#: module/setup.py:245 +#: module/setup.py:250 msgid "You can safely continue but if the webinterface is not working," msgstr "" -#: module/setup.py:246 +#: module/setup.py:251 msgid "" "please upgrade or deinstall it, pyLoad includes a sufficient jinja2 libary." msgstr "" -#: module/setup.py:263 +#: module/setup.py:268 msgid "JS engine" msgstr "" -#: module/setup.py:269 +#: module/setup.py:274 msgid "## Basic Setup ##" msgstr "" -#: module/setup.py:272 +#: module/setup.py:277 msgid "The following logindata is valid for CLI, GUI and webinterface." msgstr "" -#: module/setup.py:277 module/setup.py:366 module/setup.py:382 +#: module/setup.py:282 module/setup.py:371 module/setup.py:387 msgid "Username" msgstr "" -#: module/setup.py:283 +#: module/setup.py:288 msgid "" "External clients (GUI, CLI or other) need remote access to work over the " "network." msgstr "" -#: module/setup.py:284 +#: 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:285 +#: module/setup.py:290 msgid "Enable remote access" msgstr "" -#: module/setup.py:290 +#: module/setup.py:295 msgid "Language" msgstr "" -#: module/setup.py:293 +#: module/setup.py:298 msgid "Downloadfolder" msgstr "" -#: module/setup.py:294 +#: module/setup.py:299 msgid "Max parallel downloads" msgstr "" -#: module/setup.py:298 +#: module/setup.py:303 msgid "Use Reconnect?" msgstr "" -#: module/setup.py:301 +#: module/setup.py:306 msgid "Reconnect script location" msgstr "" -#: module/setup.py:306 +#: module/setup.py:311 msgid "## Webinterface Setup ##" msgstr "" -#: module/setup.py:309 +#: module/setup.py:314 msgid "Activate webinterface?" msgstr "" -#: module/setup.py:311 +#: 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:312 +#: module/setup.py:317 msgid "Address" msgstr "" -#: module/setup.py:313 +#: module/setup.py:318 msgid "Port" msgstr "" -#: module/setup.py:315 +#: module/setup.py:320 msgid "" "pyLoad offers several server backends, now following a short explanation." msgstr "" -#: module/setup.py:316 +#: module/setup.py:321 msgid "Default server, best choice if you dont know which one to choose." msgstr "" -#: module/setup.py:317 +#: module/setup.py:322 msgid "This server offers SSL and is a good alternative to builtin." msgstr "" -#: module/setup.py:318 +#: 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:319 +#: module/setup.py:324 msgid "Very fast alternative written in C, requires libev and linux knowlegde." msgstr "" -#: module/setup.py:320 +#: module/setup.py:325 msgid "Get it from here: https://github.com/jonashaag/bjoern, compile it" msgstr "" -#: module/setup.py:321 +#: module/setup.py:326 msgid "and copy bjoern.so to module/lib" msgstr "" -#: module/setup.py:324 +#: 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:325 +#: module/setup.py:330 msgid "come back here and change the builtin server to the threaded one here." msgstr "" -#: module/setup.py:328 +#: module/setup.py:333 msgid "Server" msgstr "" -#: module/setup.py:332 +#: module/setup.py:337 msgid "## SSL Setup ##" msgstr "" -#: module/setup.py:334 +#: module/setup.py:339 msgid "" "Execute these commands from pyLoad config folder to make ssl certificates:" msgstr "" -#: module/setup.py:340 +#: module/setup.py:345 msgid "If you're done and everything went fine, you can activate ssl now." msgstr "" -#: module/setup.py:342 +#: module/setup.py:347 msgid "Activate SSL?" msgstr "" -#: module/setup.py:356 +#: module/setup.py:361 msgid "Select action" msgstr "" -#: module/setup.py:357 +#: module/setup.py:362 msgid "1 - Create/Edit user" msgstr "" -#: module/setup.py:358 +#: module/setup.py:363 msgid "2 - List users" msgstr "" -#: module/setup.py:359 +#: module/setup.py:364 msgid "3 - Remove user" msgstr "" -#: module/setup.py:360 +#: module/setup.py:365 msgid "4 - Quit" msgstr "" -#: module/setup.py:372 +#: module/setup.py:377 msgid "Users" msgstr "" -#: module/setup.py:400 +#: module/setup.py:406 msgid "Setting new configpath, current configuration will not be transfered!" msgstr "" -#: module/setup.py:401 +#: module/setup.py:407 msgid "Configpath" msgstr "" -#: module/setup.py:409 +#: module/setup.py:415 msgid "Configpath changed, setup will now close, please restart to go on." msgstr "" -#: module/setup.py:410 +#: module/setup.py:416 msgid "Press Enter to exit." msgstr "" -#: module/setup.py:414 +#: module/setup.py:420 #, python-format msgid "Setting config path failed: %s" msgstr "" -#: module/setup.py:419 +#: module/setup.py:425 #, python-format msgid "%s: OK" msgstr "" -#: module/setup.py:421 +#: module/setup.py:427 #, python-format msgid "%s: missing" msgstr "" -#: module/setup.py:464 +#: 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:469 +#: module/setup.py:475 msgid "Password to short. Use at least 4 symbols." msgstr "" -#: module/setup.py:475 +#: module/setup.py:481 msgid "Password (again): " msgstr "" -#: module/setup.py:482 +#: module/setup.py:488 msgid "Passwords did not match." msgstr "" -#: module/setup.py:497 module/setup.py:508 +#: 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/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/setup.py b/module/setup.py index 4a1c59da6..2f6963db9 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 @@ -39,14 +39,19 @@ class Setup(): 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) + #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.") @@ -139,15 +144,15 @@ class Setup(): 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 _("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) + path = self.ask(_("Change config path?"), self.no , bool=True) if path: self.conf_path() #calls exit when changed @@ -156,7 +161,7 @@ class Setup(): 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 +169,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() @@ -282,7 +287,7 @@ 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 "" @@ -295,7 +300,7 @@ class Setup(): #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") @@ -306,7 +311,7 @@ class Setup(): 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") @@ -339,11 +344,11 @@ 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 @@ -394,7 +399,8 @@ 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!") @@ -446,10 +452,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 @@ -489,9 +495,11 @@ class Setup(): input = default if bool: - if re.match(r"(y|yes|j|ja|true)", input.lower().strip()): - return True - elif re.match(r"(n|no|nein|false)", 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 + # 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") 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/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: -- cgit v1.2.3 From 20075709afd16c7bb69b482e552f041d20a652f2 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Wed, 7 Dec 2011 20:01:46 +0100 Subject: closed #445 --- module/Api.py | 3 +- module/setup.py | 137 +++++++++++++++++++++++------------------------ module/web/pyload_app.py | 2 +- 3 files changed, 71 insertions(+), 71 deletions(-) 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/setup.py b/module/setup.py index 2f6963db9..85b33b1ee 100644 --- a/module/setup.py +++ b/module/setup.py @@ -34,38 +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) gettext.setpaths([join(os.sep, "usr", "share", "pyload", "locale"), None]) - translation = gettext.translation("setup", join(self.path, "locale"), languages=[lang, "en"],fallback=True) + translation = gettext.translation("setup", join(self.path, "locale"), languages=[lang, "en"], fallback=True) translation.install(True) #Input shorthand for yes - self.yes=_("y") + 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." + 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 "" @@ -74,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 "" @@ -95,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")) @@ -104,44 +103,44 @@ 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?"), self.yes, bool=True) @@ -151,13 +150,13 @@ class Setup(): 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?"), self.no , bool=True) + 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.") @@ -202,7 +201,6 @@ class Setup(): print _("Python Version: OK") python = True - curl = self.check_module("pycurl") self.print_dep("pycurl", curl) @@ -212,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) @@ -223,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 @@ -243,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"): @@ -251,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 @@ -275,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() @@ -289,12 +287,10 @@ class Setup(): 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"), 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.") @@ -309,7 +305,7 @@ class Setup(): def conf_web(self): print "" print _("## Webinterface Setup ##") - + print "" self.config["webinterface"]["activated"] = self.ask(_("Activate webinterface?"), self.yes, bool=True) print "" @@ -320,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 "" @@ -348,13 +346,15 @@ class Setup(): def set_user(self): 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 = 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: @@ -368,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 @@ -400,16 +400,17 @@ class Setup(): def conf_path(self, trans=False): if trans: 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 = 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.") @@ -418,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: @@ -463,7 +464,6 @@ class Setup(): p1 = True p2 = False while p1 != p2: - if os.name == "nt": qst = str("Password: ") #no unicode on windows else: @@ -495,17 +495,16 @@ class Setup(): input = default if bool: - # yes, true,t are inputs for booleans with value true - if input.lower().strip() in [self.yes, _("yes"), _("true"), _("t")]: - return True - # no, false,f are inputs for booleans with value false + # yes, true,t are inputs for booleans with value true + if input.lower().strip() in [self.yes, _("yes"), _("true"), _("t")]: + return True + # 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') -- cgit v1.2.3 From 52b600e13c7b18571af4317326a16d83a8f257ae Mon Sep 17 00:00:00 2001 From: RaNaN Date: Wed, 7 Dec 2011 21:32:49 +0100 Subject: update readme file --- README | 95 ++++++++++++++++++------------- module/remote/thriftbackend/pyload.thrift | 57 ++++++++++++++++--- 2 files changed, 106 insertions(+), 46 deletions(-) diff --git a/README b/README index f0d3b675d..2965f3f32 100644 --- a/README +++ b/README @@ -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,81 @@ 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 +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 +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 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 . +the options take a look at +For more information, see http://pyload.org/ 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 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 fids } +struct InteractionTask { + 1: InteractionID iid, + 2: Input input, + 3: list structure, + 4: list preset, + 5: Output output, + 6: list 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 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 getEvents(1: string uuid) @@ -289,8 +320,18 @@ service Pyload { //info // {plugin: {name: value}} map> getAllInfo(), - map getInfoByPlugin(1: PluginName plugin) + map 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), } -- cgit v1.2.3 From 206a294aa8e8f859ed425ab1054e0a18fc3ad602 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Wed, 7 Dec 2011 22:17:38 +0100 Subject: updated bottle.py --- README | 3 +- module/lib/bottle.py | 2026 ++++++++++++-------- .../thriftbackend/thriftgen/pyload/Pyload-remote | 58 +- .../thriftbackend/thriftgen/pyload/Pyload.py | 810 ++++---- .../thriftbackend/thriftgen/pyload/constants.py | 2 +- .../thriftbackend/thriftgen/pyload/ttypes.py | 108 +- pavement.py | 5 +- 7 files changed, 1761 insertions(+), 1251 deletions(-) diff --git a/README b/README index 2965f3f32..b0defe839 100644 --- a/README +++ b/README @@ -42,6 +42,7 @@ Optional - jsengine (spidermonkey, ossp-js, pyv8, rhino): Used for several hoster, ClickNLoad - feedparser - BeautifulSoup +- pyOpenSSL: For SSL connection First start =========== @@ -64,7 +65,7 @@ Configuration 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= (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/`). The wildcard syntax + and details on the matching order are described in docs:`routing`. ''' - default = '[^/]+' - - @lazy_attribute - def syntax(cls): - return re.compile(r'(?]+)+)?)?)?>))') + + 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__': '', '__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') - - >>> _load('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 = """ Error {{e.status}}: {{status_name}}

Error {{e.status}}: {{status_name}}

-

Sorry, the requested URL {{repr(request.url)}} caused an error:

+

Sorry, the requested URL {{repr(request.url)}} + caused an error:

{{e.output}}
%if DEBUG and e.exception:

Exception:

@@ -2499,17 +2877,18 @@ ERROR_PAGE_TEMPLATE = """ %except ImportError: - ImportError: Could not generate the error page. Please add bottle to sys.path + ImportError: 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/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,90 +2303,205 @@ 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() -class Processor(Iface, TProcessor): - def __init__(self, handler): - self._handler = handler - self._processMap = {} - self._processMap["getConfigValue"] = Processor.process_getConfigValue - self._processMap["setConfigValue"] = Processor.process_setConfigValue - self._processMap["getConfig"] = Processor.process_getConfig - self._processMap["getPluginConfig"] = Processor.process_getPluginConfig - self._processMap["pauseServer"] = Processor.process_pauseServer - self._processMap["unpauseServer"] = Processor.process_unpauseServer - self._processMap["togglePause"] = Processor.process_togglePause - self._processMap["statusServer"] = Processor.process_statusServer - self._processMap["freeSpace"] = Processor.process_freeSpace - self._processMap["getServerVersion"] = Processor.process_getServerVersion - self._processMap["kill"] = Processor.process_kill - self._processMap["restart"] = Processor.process_restart - self._processMap["getLog"] = Processor.process_getLog - self._processMap["isTimeDownload"] = Processor.process_isTimeDownload - self._processMap["isTimeReconnect"] = Processor.process_isTimeReconnect - self._processMap["toggleReconnect"] = Processor.process_toggleReconnect - self._processMap["generatePackages"] = Processor.process_generatePackages - self._processMap["checkURLs"] = Processor.process_checkURLs - self._processMap["parseURLs"] = Processor.process_parseURLs - self._processMap["checkOnlineStatus"] = Processor.process_checkOnlineStatus - self._processMap["checkOnlineStatusContainer"] = Processor.process_checkOnlineStatusContainer - self._processMap["pollResults"] = Processor.process_pollResults - self._processMap["statusDownloads"] = Processor.process_statusDownloads - self._processMap["getPackageData"] = Processor.process_getPackageData - self._processMap["getPackageInfo"] = Processor.process_getPackageInfo - self._processMap["getFileData"] = Processor.process_getFileData - self._processMap["getQueue"] = Processor.process_getQueue - self._processMap["getCollector"] = Processor.process_getCollector - self._processMap["getQueueData"] = Processor.process_getQueueData - self._processMap["getCollectorData"] = Processor.process_getCollectorData - self._processMap["getPackageOrder"] = Processor.process_getPackageOrder - self._processMap["getFileOrder"] = Processor.process_getFileOrder - self._processMap["generateAndAddPackages"] = Processor.process_generateAndAddPackages - self._processMap["addPackage"] = Processor.process_addPackage - self._processMap["addFiles"] = Processor.process_addFiles - self._processMap["uploadContainer"] = Processor.process_uploadContainer - self._processMap["deleteFiles"] = Processor.process_deleteFiles - self._processMap["deletePackages"] = Processor.process_deletePackages - self._processMap["pushToQueue"] = Processor.process_pushToQueue - self._processMap["pullFromQueue"] = Processor.process_pullFromQueue - self._processMap["restartPackage"] = Processor.process_restartPackage - self._processMap["restartFile"] = Processor.process_restartFile - self._processMap["recheckPackage"] = Processor.process_recheckPackage - self._processMap["stopAllDownloads"] = Processor.process_stopAllDownloads - self._processMap["stopDownloads"] = Processor.process_stopDownloads - self._processMap["setPackageName"] = Processor.process_setPackageName - self._processMap["movePackage"] = Processor.process_movePackage - self._processMap["moveFiles"] = Processor.process_moveFiles - self._processMap["orderPackage"] = Processor.process_orderPackage - self._processMap["orderFile"] = Processor.process_orderFile - 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 - self._processMap["updateAccount"] = Processor.process_updateAccount - self._processMap["removeAccount"] = Processor.process_removeAccount - self._processMap["login"] = Processor.process_login - self._processMap["getUserData"] = Processor.process_getUserData - self._processMap["getAllUserData"] = Processor.process_getAllUserData - self._processMap["getServices"] = Processor.process_getServices - self._processMap["hasService"] = Processor.process_hasService - self._processMap["call"] = Processor.process_call - self._processMap["getAllInfo"] = Processor.process_getAllInfo - self._processMap["getInfoByPlugin"] = Processor.process_getInfoByPlugin + 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 process(self, iprot, oprot): - (name, type, seqid) = iprot.readMessageBegin() - if name not in self._processMap: - iprot.skip(TType.STRUCT) - iprot.readMessageEnd() - x = TApplicationException(TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % (name)) - oprot.writeMessageBegin(name, TMessageType.EXCEPTION, seqid) - x.write(oprot) + 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): + self._handler = handler + self._processMap = {} + self._processMap["getConfigValue"] = Processor.process_getConfigValue + self._processMap["setConfigValue"] = Processor.process_setConfigValue + self._processMap["getConfig"] = Processor.process_getConfig + self._processMap["getPluginConfig"] = Processor.process_getPluginConfig + self._processMap["pauseServer"] = Processor.process_pauseServer + self._processMap["unpauseServer"] = Processor.process_unpauseServer + self._processMap["togglePause"] = Processor.process_togglePause + self._processMap["statusServer"] = Processor.process_statusServer + self._processMap["freeSpace"] = Processor.process_freeSpace + self._processMap["getServerVersion"] = Processor.process_getServerVersion + self._processMap["kill"] = Processor.process_kill + self._processMap["restart"] = Processor.process_restart + self._processMap["getLog"] = Processor.process_getLog + self._processMap["isTimeDownload"] = Processor.process_isTimeDownload + self._processMap["isTimeReconnect"] = Processor.process_isTimeReconnect + self._processMap["toggleReconnect"] = Processor.process_toggleReconnect + self._processMap["generatePackages"] = Processor.process_generatePackages + self._processMap["checkURLs"] = Processor.process_checkURLs + self._processMap["parseURLs"] = Processor.process_parseURLs + self._processMap["checkOnlineStatus"] = Processor.process_checkOnlineStatus + self._processMap["checkOnlineStatusContainer"] = Processor.process_checkOnlineStatusContainer + self._processMap["pollResults"] = Processor.process_pollResults + self._processMap["statusDownloads"] = Processor.process_statusDownloads + self._processMap["getPackageData"] = Processor.process_getPackageData + self._processMap["getPackageInfo"] = Processor.process_getPackageInfo + self._processMap["getFileData"] = Processor.process_getFileData + self._processMap["getQueue"] = Processor.process_getQueue + self._processMap["getCollector"] = Processor.process_getCollector + self._processMap["getQueueData"] = Processor.process_getQueueData + self._processMap["getCollectorData"] = Processor.process_getCollectorData + self._processMap["getPackageOrder"] = Processor.process_getPackageOrder + self._processMap["getFileOrder"] = Processor.process_getFileOrder + self._processMap["generateAndAddPackages"] = Processor.process_generateAndAddPackages + self._processMap["addPackage"] = Processor.process_addPackage + self._processMap["addFiles"] = Processor.process_addFiles + self._processMap["uploadContainer"] = Processor.process_uploadContainer + self._processMap["deleteFiles"] = Processor.process_deleteFiles + self._processMap["deletePackages"] = Processor.process_deletePackages + self._processMap["pushToQueue"] = Processor.process_pushToQueue + self._processMap["pullFromQueue"] = Processor.process_pullFromQueue + self._processMap["restartPackage"] = Processor.process_restartPackage + self._processMap["restartFile"] = Processor.process_restartFile + self._processMap["recheckPackage"] = Processor.process_recheckPackage + self._processMap["stopAllDownloads"] = Processor.process_stopAllDownloads + self._processMap["stopDownloads"] = Processor.process_stopDownloads + self._processMap["setPackageName"] = Processor.process_setPackageName + self._processMap["movePackage"] = Processor.process_movePackage + self._processMap["moveFiles"] = Processor.process_moveFiles + self._processMap["orderPackage"] = Processor.process_orderPackage + self._processMap["orderFile"] = Processor.process_orderFile + self._processMap["setPackageData"] = Processor.process_setPackageData + self._processMap["deleteFinished"] = Processor.process_deleteFinished + self._processMap["restartFailed"] = Processor.process_restartFailed + self._processMap["getEvents"] = Processor.process_getEvents + self._processMap["getAccounts"] = Processor.process_getAccounts + self._processMap["getAccountTypes"] = Processor.process_getAccountTypes + self._processMap["updateAccount"] = Processor.process_updateAccount + self._processMap["removeAccount"] = Processor.process_removeAccount + self._processMap["login"] = Processor.process_login + self._processMap["getUserData"] = Processor.process_getUserData + self._processMap["getAllUserData"] = Processor.process_getAllUserData + self._processMap["getServices"] = Processor.process_getServices + self._processMap["hasService"] = Processor.process_hasService + 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() + if name not in self._processMap: + iprot.skip(TType.STRUCT) + iprot.readMessageEnd() + x = TApplicationException(TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % (name)) + oprot.writeMessageBegin(name, TMessageType.EXCEPTION, seqid) + x.write(oprot) oprot.writeMessageEnd() oprot.trans.flush() return @@ -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,12 +3252,56 @@ 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() -# HELPER FUNCTIONS AND STRUCTURES + 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() -class getConfigValue_args(TBase): - """ - Attributes: + 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 + +class getConfigValue_args(TBase): + """ + Attributes: - category - option - section @@ -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/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={ -- cgit v1.2.3