├── .nose.cfg ├── .travis.yml ├── LICENSE ├── README.mkd ├── README_fixtures.py ├── TUTORIAL.mkd ├── TUTORIAL_fixtures.py ├── beanstalkc.py ├── setup.py └── test ├── __init__.py ├── fixtures.py ├── no-yaml.mkd └── no-yaml_fixtures.py /.nose.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | verbosity=3 3 | 4 | with-doctest=1 5 | doctest-extension=mkd 6 | doctest-fixtures=_fixtures 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # .travis.yaml contains YAML-formatted (http://www.yaml.org/) build 3 | # instructions for continuous integration via Travis CI 4 | # (http://docs.travis-ci.com/). 5 | # 6 | 7 | language: python 8 | 9 | addons: 10 | apt: 11 | sources: 12 | - deadsnakes 13 | packages: 14 | - python2.4-dev 15 | - python2.5-dev 16 | - python2.6-dev 17 | - python2.7-dev 18 | 19 | env: 20 | global: 21 | - BEANSTALKD=./beanstalkd 22 | matrix: 23 | - PYV=2.4 ST=https://raw.githubusercontent.com/pypa/setuptools/bootstrap-py24/ez_setup.py 24 | - PYV=2.5 ST=https://raw.githubusercontent.com/pypa/setuptools/bootstrap-py24/ez_setup.py 25 | - PYV=2.6 ST=https://bootstrap.pypa.io/ez_setup.py 26 | - PYV=2.7 ST=https://bootstrap.pypa.io/ez_setup.py 27 | 28 | install: 29 | # Deactivate the virtualenv enabled by `language: python`. 30 | - deactivate 31 | # Install most recent beanstalkd from source 32 | - wget https://github.com/kr/beanstalkd/archive/v1.10.tar.gz 33 | - tar xf v1.10.tar.gz 34 | - make -C beanstalkd-1.10/ 35 | - mv beanstalkd-1.10/beanstalkd . 36 | # Bootstrap easy_install ... 37 | - wget -O /tmp/ez_setup.py $ST 38 | - sudo python$PYV /tmp/ez_setup.py 39 | # ... to use easy_install to install pip (1.1 works on 2.4) ... 40 | - sudo easy_install-$PYV pip==1.1 41 | # ... to use pip to install beanstalkc's dependencies ... 42 | - sudo pip-$PYV install http://pyyaml.org/download/pyyaml/PyYAML-3.09.tar.gz 43 | # ... and tes testing dependencies. 44 | - sudo pip-$PYV install nose 45 | 46 | script: nosetests-$PYV -c .nose.cfg 47 | 48 | branches: 49 | only: 50 | - master 51 | - preflight 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | beanstalkc 2 | ========== 3 | 4 | beanstalkc is a simple beanstalkd client library for Python. [beanstalkd][] is 5 | a fast, distributed, in-memory workqueue service. 6 | 7 | beanstalkc depends on [PyYAML][], but there are ways to avoid this dependency. 8 | See Appendix A of the tutorial for details. 9 | 10 | beanstalkc is pure Python, and is compatible with [eventlet][] and [gevent][]. 11 | 12 | beanstalkc is currently only supported on Python 2 and automatically tested 13 | against Python 2.6 and 2.7. Python 3 is not (yet) supported. 14 | 15 | [beanstalkd]: http://kr.github.com/beanstalkd/ 16 | [eventlet]: http://eventlet.net/ 17 | [gevent]: http://www.gevent.org/ 18 | [PyYAML]: http://pyyaml.org/ 19 | 20 | 21 | Usage 22 | ----- 23 | 24 | Here is a short example, to illustrate the flavor of beanstalkc: 25 | 26 | >>> import beanstalkc 27 | >>> beanstalk = beanstalkc.Connection(host='localhost', port=14711) 28 | >>> beanstalk.put('hey!') 29 | 1 30 | >>> job = beanstalk.reserve() 31 | >>> job.body 32 | 'hey!' 33 | >>> job.delete() 34 | 35 | For more information, see [the tutorial](TUTORIAL.mkd), which will explain most 36 | everything. 37 | 38 | 39 | License 40 | ------- 41 | 42 | Copyright (C) 2008-2016 Andreas Bolka, Licensed under the [Apache License, 43 | Version 2.0][license]. 44 | 45 | [license]: http://www.apache.org/licenses/LICENSE-2.0 46 | -------------------------------------------------------------------------------- /README_fixtures.py: -------------------------------------------------------------------------------- 1 | from test.fixtures import setup, teardown 2 | -------------------------------------------------------------------------------- /TUTORIAL.mkd: -------------------------------------------------------------------------------- 1 | beanstalkc Tutorial 2 | =================== 3 | 4 | Welcome, dear stranger, to a tour de force through beanstalkd's capabilities. 5 | Say hello to your fellow travel companion, the beanstalkc client library for 6 | Python. You'll get to know each other fairly well during this trip, so better 7 | start off on a friendly note. And now, let's go! 8 | 9 | 10 | Getting Started 11 | --------------- 12 | 13 | You'll need beanstalkd listening at port 14711 to follow along. So simply start 14 | it using: `beanstalkd -l 127.0.0.1 -p 14711` 15 | 16 | Besides having beanstalkc installed, you'll typically also need PyYAML. If you 17 | insist, you can also use beanstalkc without PyYAML. For more details see 18 | Appendix A of this tutorial. 19 | 20 | To use beanstalkc we have to import the library and set up a connection to an 21 | (already running) beanstalkd server: 22 | 23 | >>> import beanstalkc 24 | >>> beanstalk = beanstalkc.Connection(host='localhost', port=14711) 25 | 26 | If we leave out the `host` and/or `port` parameters, `'localhost'` and `11300` 27 | would be used as defaults, respectively. There is also a `connect_timeout` 28 | parameter which determines how long, in seconds, the socket will wait for the 29 | server to respond to its initial connection attempt. If it is `None`, then 30 | there will be no timeout; it defaults to the result of your system's 31 | `socket.getdefaulttimeout()`. 32 | 33 | 34 | Basic Operation 35 | --------------- 36 | 37 | Now that we have a connection set up, we can enqueue jobs: 38 | 39 | >>> beanstalk.put('hey!') 40 | 1 41 | 42 | Or we can request jobs: 43 | 44 | >>> job = beanstalk.reserve() 45 | >>> job.body 46 | 'hey!' 47 | 48 | Once we are done with processing a job, we have to mark it as done, otherwise 49 | jobs are re-queued by beanstalkd after a "time to run" (120 seconds, per 50 | default) is surpassed. A job is marked as done, by calling `delete`: 51 | 52 | >>> job.delete() 53 | 54 | `reserve` blocks until a job is ready, possibly forever. If that is not desired, 55 | we can invoke `reserve` with a timeout (in seconds) how long we want to wait to 56 | receive a job. If such a `reserve` times out, it will return `None`: 57 | 58 | >>> beanstalk.reserve(timeout=0) is None 59 | True 60 | 61 | If you use a timeout of 0, `reserve` will immediately return either a job or 62 | `None`. 63 | 64 | Note that beanstalkc requires job bodies to be strings, conversion to/from 65 | strings is left up to you: 66 | 67 | >>> beanstalk.put(42) 68 | Traceback (most recent call last): 69 | ... 70 | AssertionError: Job body must be a str instance 71 | 72 | There is no restriction on what characters you can put in a job body, so they 73 | can be used to hold arbitrary binary data: 74 | 75 | >>> _ = beanstalk.put('\x00\x01\xfe\xff') 76 | >>> job = beanstalk.reserve() ; print(repr(job.body)) ; job.delete() 77 | '\x00\x01\xfe\xff' 78 | 79 | If you want to send images, just `put` the image data as a string. If you want 80 | to send Unicode text, just use `unicode.encode` to convert it to a string with 81 | some encoding. 82 | 83 | 84 | Tube Management 85 | --------------- 86 | 87 | A single beanstalkd server can provide many different queues, called "tubes" in 88 | beanstalkd. To see all available tubes: 89 | 90 | >>> beanstalk.tubes() 91 | ['default'] 92 | 93 | A beanstalkd client can choose one tube into which its job are put. This is the 94 | tube "used" by the client. To see what tube you are currently using: 95 | 96 | >>> beanstalk.using() 97 | 'default' 98 | 99 | Unless told otherwise, a client uses the 'default' tube. If you want to use a 100 | different tube: 101 | 102 | >>> beanstalk.use('foo') 103 | 'foo' 104 | >>> beanstalk.using() 105 | 'foo' 106 | 107 | If you decide to use a tube, that does not yet exist, the tube is automatically 108 | created by beanstalkd: 109 | 110 | >>> beanstalk.tubes() 111 | ['default', 'foo'] 112 | 113 | Of course, you can always switch back to the default tube. Tubes that don't have 114 | any client using or watching, vanish automatically: 115 | 116 | >>> beanstalk.use('default') 117 | 'default' 118 | >>> beanstalk.using() 119 | 'default' 120 | >>> beanstalk.tubes() 121 | ['default'] 122 | 123 | Further, a beanstalkd client can choose many tubes to reserve jobs from. These 124 | tubes are "watched" by the client. To see what tubes you are currently watching: 125 | 126 | >>> beanstalk.watching() 127 | ['default'] 128 | 129 | To watch an additional tube: 130 | 131 | >>> beanstalk.watch('bar') 132 | 2 133 | >>> beanstalk.watching() 134 | ['default', 'bar'] 135 | 136 | As before, tubes that do not yet exist are created automatically once you start 137 | watching them: 138 | 139 | >>> beanstalk.tubes() 140 | ['default', 'bar'] 141 | 142 | To stop watching a tube: 143 | 144 | >>> beanstalk.ignore('bar') 145 | 1 146 | >>> beanstalk.watching() 147 | ['default'] 148 | 149 | You can't watch zero tubes. So if you try to ignore the last tube you are 150 | watching, this is silently ignored: 151 | 152 | >>> beanstalk.ignore('default') 153 | 0 154 | >>> beanstalk.watching() 155 | ['default'] 156 | 157 | To recap: each beanstalkd client manages two separate concerns: which tube 158 | newly created jobs are put into, and which tube(s) jobs are reserved from. 159 | Accordingly, there are two separate sets of functions for these concerns: 160 | 161 | - `use` and `using` affect where jobs are `put`; 162 | - `watch` and `watching` control where jobs are `reserve`d from. 163 | 164 | Note that these concerns are fully orthogonal: for example, when you `use` a 165 | tube, it is not automatically `watch`ed. Neither does `watch`ing a tube affect 166 | the tube you are `using`. 167 | 168 | 169 | Statistics 170 | ---------- 171 | 172 | Beanstalkd accumulates various statistics at the server, tube and job level. 173 | Statistical details for a job can only be retrieved during the job's lifecycle. 174 | So let's create another job: 175 | 176 | >>> beanstalk.put('ho?') 177 | 3 178 | 179 | >>> job = beanstalk.reserve() 180 | 181 | Now we retrieve job-level statistics: 182 | 183 | >>> from pprint import pprint 184 | >>> pprint(job.stats()) # doctest: +ELLIPSIS 185 | {'age': 0, 186 | ... 187 | 'id': 3, 188 | ... 189 | 'state': 'reserved', 190 | ... 191 | 'tube': 'default'} 192 | 193 | If you try to access job stats after the job was deleted, you'll get a 194 | `CommandFailed` exception: 195 | 196 | >>> job.delete() 197 | >>> job.stats() # doctest: +IGNORE_EXCEPTION_DETAIL 198 | Traceback (most recent call last): 199 | CommandFailed: ('stats-job', 'NOT_FOUND', []) 200 | 201 | Let's have a look at some numbers for the `'default'` tube: 202 | 203 | >>> pprint(beanstalk.stats_tube('default')) # doctest: +ELLIPSIS 204 | {... 205 | 'current-jobs-ready': 0, 206 | 'current-jobs-reserved': 0, 207 | 'current-jobs-urgent': 0, 208 | ... 209 | 'name': 'default', 210 | ...} 211 | 212 | Finally, there's an abundant amount of server-level statistics accessible via 213 | the `Connection`'s `stats` method. We won't go into details here, but: 214 | 215 | >>> pprint(beanstalk.stats()) # doctest: +ELLIPSIS 216 | {... 217 | 'current-connections': 1, 218 | 'current-jobs-buried': 0, 219 | 'current-jobs-delayed': 0, 220 | 'current-jobs-ready': 0, 221 | 'current-jobs-reserved': 0, 222 | 'current-jobs-urgent': 0, 223 | ...} 224 | 225 | 226 | Advanced Operation 227 | ------------------ 228 | 229 | In "Basic Operation" above, we discussed the typical lifecycle of a job: 230 | 231 | put reserve delete 232 | -----> [READY] ---------> [RESERVED] --------> *poof* 233 | 234 | 235 | (This picture was taken from beanstalkd's protocol documentation. It is 236 | originally contained in `protocol.txt`, part of the beanstalkd 237 | distribution.) #doctest:+SKIP 238 | 239 | But besides `ready` and `reserved`, a job can also be `delayed` or `buried`. 240 | Along with those states come a few transitions, so the full picture looks like 241 | the following: 242 | 243 | put with delay release with delay 244 | ----------------> [DELAYED] <------------. 245 | | | 246 | | (time passes) | 247 | | | 248 | put v reserve | delete 249 | -----------------> [READY] ---------> [RESERVED] --------> *poof* 250 | ^ ^ | | 251 | | \ release | | 252 | | `-------------' | 253 | | | 254 | | kick | 255 | | | 256 | | bury | 257 | [BURIED] <---------------' 258 | | 259 | | delete 260 | `--------> *poof* 261 | 262 | 263 | (This picture was taken from beanstalkd's protocol documentation. It is 264 | originally contained in `protocol.txt`, part of the beanstalkd 265 | distribution.) #doctest:+SKIP 266 | 267 | Now let's have a practical look at those new possibilities. For a start, we can 268 | create a job with a delay. Such a job will only be available for reservation 269 | once this delay passes: 270 | 271 | >>> beanstalk.put('yes!', delay=1) 272 | 4 273 | 274 | >>> beanstalk.reserve(timeout=0) is None 275 | True 276 | 277 | >>> job = beanstalk.reserve(timeout=1) 278 | >>> job.body 279 | 'yes!' 280 | 281 | If we are not interested in a job anymore (e.g. after we failed to 282 | process it), we can simply release the job back to the tube it came from: 283 | 284 | >>> job.release() 285 | >>> job.stats()['state'] 286 | 'ready' 287 | 288 | Want to get rid of a job? Well, just "bury" it! A buried job is put aside and is 289 | therefore not available for reservation anymore: 290 | 291 | >>> job = beanstalk.reserve() 292 | >>> job.bury() 293 | >>> job.stats()['state'] 294 | 'buried' 295 | 296 | >>> beanstalk.reserve(timeout=0) is None 297 | True 298 | 299 | Buried jobs are maintained in a special FIFO-queue outside of the normal job 300 | processing lifecycle until they are kicked alive again: 301 | 302 | >>> beanstalk.kick() 303 | 1 304 | 305 | You can request many jobs to be kicked alive at once, `kick`'s return value will 306 | tell you how many jobs were actually kicked alive again: 307 | 308 | >>> beanstalk.kick(42) 309 | 0 310 | 311 | If you still have a handle to a job (or know its job ID), you can also kick 312 | alive this particular job, overriding the FIFO operation of the burial queue: 313 | 314 | >>> job = beanstalk.reserve() 315 | >>> job.bury() 316 | >>> job.stats()['state'] 317 | 'buried' 318 | >>> job.kick() 319 | >>> job.stats()['state'] 320 | 'ready' 321 | 322 | (NOTE: The `kick-job` command was introduced in beanstalkd v1.8.) 323 | 324 | 325 | Inspecting Jobs 326 | --------------- 327 | 328 | Besides reserving jobs, a client can also "peek" at jobs. This allows to inspect 329 | jobs without modifying their state. If you know the `id` of a job you're 330 | interested, you can directly peek at the job. We still have job #4 hanging 331 | around from our previous examples, so: 332 | 333 | >>> job = beanstalk.peek(4) 334 | >>> job.body 335 | 'yes!' 336 | 337 | Note that this peek did *not* reserve the job: 338 | 339 | >>> job.stats()['state'] 340 | 'ready' 341 | 342 | If you try to peek at a non-existing job, you'll simply see nothing: 343 | 344 | >>> beanstalk.peek(42) is None 345 | True 346 | 347 | If you are not interested in a particular job, but want to see jobs according to 348 | their state, beanstalkd also provides a few commands. To peek at the same job 349 | that would be returned by `reserve` -- the next ready job -- use `peek-ready`: 350 | 351 | >>> job = beanstalk.peek_ready() 352 | >>> job.body 353 | 'yes!' 354 | 355 | Note that you can't release, or bury a job that was not reserved by you. Those 356 | requests on unreserved jobs are silently ignored: 357 | 358 | >>> job.release() 359 | >>> job.bury() 360 | 361 | >>> job.stats()['state'] 362 | 'ready' 363 | 364 | You can, though, delete a job that was not reserved by you: 365 | 366 | >>> job.delete() 367 | >>> job.stats() # doctest: +IGNORE_EXCEPTION_DETAIL 368 | Traceback (most recent call last): 369 | CommandFailed: ('stats-job', 'NOT_FOUND', []) 370 | 371 | Finally, you can also peek into the special queues for jobs that are delayed: 372 | 373 | >>> _ = beanstalk.put('o tempores', delay=120) 374 | >>> job = beanstalk.peek_delayed() 375 | >>> job.stats()['state'] 376 | 'delayed' 377 | 378 | ... or buried: 379 | 380 | >>> _ = beanstalk.put('o mores!') 381 | >>> job = beanstalk.reserve() 382 | >>> job.bury() 383 | 384 | >>> job = beanstalk.peek_buried() 385 | >>> job.stats()['state'] 386 | 'buried' 387 | 388 | 389 | Job Priorities 390 | -------------- 391 | 392 | Without job priorities, beanstalkd operates as a FIFO queue: 393 | 394 | >>> _ = beanstalk.put('1') 395 | >>> _ = beanstalk.put('2') 396 | 397 | >>> job = beanstalk.reserve() ; print(job.body) ; job.delete() 398 | 1 399 | >>> job = beanstalk.reserve() ; print(job.body) ; job.delete() 400 | 2 401 | 402 | If need arises, you can override this behaviour by giving different jobs 403 | different priorities. There are three hard facts to know about job priorities: 404 | 405 | 1. Jobs with lower priority numbers are reserved before jobs with higher 406 | priority numbers. 407 | 408 | 2. beanstalkd priorities are 32-bit unsigned integers (they range from 0 to 409 | 2**32 - 1). 410 | 411 | 3. beanstalkc uses 2**31 as default job priority 412 | (`beanstalkc.DEFAULT_PRIORITY`). 413 | 414 | To create a job with a custom priority, use the keyword-argument `priority`: 415 | 416 | >>> _ = beanstalk.put('foo', priority=42) 417 | >>> _ = beanstalk.put('bar', priority=21) 418 | >>> _ = beanstalk.put('qux', priority=21) 419 | 420 | >>> job = beanstalk.reserve() ; print(job.body) ; job.delete() 421 | bar 422 | >>> job = beanstalk.reserve() ; print(job.body) ; job.delete() 423 | qux 424 | >>> job = beanstalk.reserve() ; print(job.body) ; job.delete() 425 | foo 426 | 427 | Note how `'bar'` and `'qux'` left the queue before `'foo'`, even though they 428 | were enqueued well after `'foo'`. Note also that within the same priority jobs 429 | are still handled in a FIFO manner. 430 | 431 | 432 | Fin! 433 | ---- 434 | 435 | >>> beanstalk.close() 436 | 437 | That's it, for now. We've left a few capabilities untouched (touch and 438 | time-to-run). But if you've really read through all of the above, send me a 439 | message and tell me what you think of it. And then go get yourself a treat. You 440 | certainly deserve it. 441 | 442 | 443 | Appendix A: beanstalkc and YAML 444 | ------------------------------- 445 | 446 | As beanstalkd uses YAML for diagnostic information (like the results of 447 | `stats()` or `tubes()`), you'll typically need [PyYAML](). Depending on your 448 | performance needs, you may want to supplement that with the [libyaml]() C 449 | extension. 450 | 451 | [PyYAML]: http://pyyaml.org/ 452 | [libyaml]: http://pyyaml.org/wiki/LibYAML 453 | 454 | If, for whatever reason, you cannot use PyYAML, you can still use beanstalkc 455 | and just leave the YAML responses unparsed. To do that, pass `parse_yaml=False` 456 | when creating the `Connection`: 457 | 458 | >>> beanstalk = beanstalkc.Connection(host='localhost', 459 | ... port=14711, 460 | ... parse_yaml=False) 461 | 462 | >>> beanstalk.tubes() 463 | '---\n- default\n' 464 | 465 | >>> beanstalk.stats_tube('default') # doctest: +ELLIPSIS 466 | '---\nname: default\ncurrent-jobs-urgent: 0\ncurrent-jobs-ready: 0\n...' 467 | 468 | >>> beanstalk.close() 469 | 470 | This possibility is mostly useful if you don't use the introspective 471 | capabilities of beanstalkd (`Connection#tubes`, `Connection#watching`, 472 | `Connection#stats`, `Connection#stats_tube`, and `Job#stats`). 473 | 474 | Alternatively, you can also pass a function to be used as YAML parser: 475 | 476 | >>> beanstalk = beanstalkc.Connection(host='localhost', 477 | ... port=14711, 478 | ... parse_yaml=lambda x: x.split('\n')) 479 | 480 | >>> beanstalk.tubes() 481 | ['---', '- default', ''] 482 | 483 | >>> beanstalk.stats_tube('default') # doctest: +ELLIPSIS 484 | ['---', 'name: default', 'current-jobs-urgent: 0', ...] 485 | 486 | >>> beanstalk.close() 487 | 488 | This should come in handy if PyYAML simply does not fit your needs. 489 | -------------------------------------------------------------------------------- /TUTORIAL_fixtures.py: -------------------------------------------------------------------------------- 1 | from test.fixtures import setup, teardown 2 | -------------------------------------------------------------------------------- /beanstalkc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """beanstalkc - A beanstalkd Client Library for Python""" 3 | 4 | import logging 5 | import socket 6 | import sys 7 | 8 | 9 | __license__ = ''' 10 | Copyright (C) 2008-2016 Andreas Bolka 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | ''' 24 | 25 | __version__ = '0.4.0' 26 | 27 | 28 | DEFAULT_HOST = 'localhost' 29 | DEFAULT_PORT = 11300 30 | DEFAULT_PRIORITY = 2 ** 31 31 | DEFAULT_TTR = 120 32 | DEFAULT_TUBE_NAME = 'default' 33 | 34 | 35 | class BeanstalkcException(Exception): pass 36 | class UnexpectedResponse(BeanstalkcException): pass 37 | class CommandFailed(BeanstalkcException): pass 38 | class DeadlineSoon(BeanstalkcException): pass 39 | 40 | class SocketError(BeanstalkcException): 41 | @staticmethod 42 | def wrap(wrapped_function, *args, **kwargs): 43 | try: 44 | return wrapped_function(*args, **kwargs) 45 | except socket.error: 46 | err = sys.exc_info()[1] 47 | raise SocketError(err) 48 | 49 | 50 | class Connection(object): 51 | def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, parse_yaml=True, 52 | connect_timeout=socket.getdefaulttimeout()): 53 | if parse_yaml is True: 54 | try: 55 | parse_yaml = __import__('yaml').load 56 | except ImportError: 57 | logging.error('Failed to load PyYAML, will not parse YAML') 58 | parse_yaml = False 59 | self._connect_timeout = connect_timeout 60 | self._parse_yaml = parse_yaml or (lambda x: x) 61 | self.host = host 62 | self.port = port 63 | self.connect() 64 | 65 | def __enter__(self): 66 | return self 67 | 68 | def __exit__(self, exc_type, exc_value, traceback): 69 | self.close() 70 | 71 | def connect(self): 72 | """Connect to beanstalkd server.""" 73 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 74 | self._socket.settimeout(self._connect_timeout) 75 | SocketError.wrap(self._socket.connect, (self.host, self.port)) 76 | self._socket.settimeout(None) 77 | self._socket_file = self._socket.makefile('rb') 78 | 79 | def close(self): 80 | """Close connection to server.""" 81 | try: 82 | self._socket.sendall('quit\r\n') 83 | except socket.error: 84 | pass 85 | try: 86 | self._socket.close() 87 | except socket.error: 88 | pass 89 | 90 | def reconnect(self): 91 | """Re-connect to server.""" 92 | self.close() 93 | self.connect() 94 | 95 | def _interact(self, command, expected_ok, expected_err=[]): 96 | SocketError.wrap(self._socket.sendall, command) 97 | status, results = self._read_response() 98 | if status in expected_ok: 99 | return results 100 | elif status in expected_err: 101 | raise CommandFailed(command.split()[0], status, results) 102 | else: 103 | raise UnexpectedResponse(command.split()[0], status, results) 104 | 105 | def _read_response(self): 106 | line = SocketError.wrap(self._socket_file.readline) 107 | if not line: 108 | raise SocketError() 109 | response = line.split() 110 | return response[0], response[1:] 111 | 112 | def _read_body(self, size): 113 | body = SocketError.wrap(self._socket_file.read, size) 114 | SocketError.wrap(self._socket_file.read, 2) # trailing crlf 115 | if size > 0 and not body: 116 | raise SocketError() 117 | return body 118 | 119 | def _interact_value(self, command, expected_ok, expected_err=[]): 120 | return self._interact(command, expected_ok, expected_err)[0] 121 | 122 | def _interact_job(self, command, expected_ok, expected_err, reserved=True): 123 | jid, size = self._interact(command, expected_ok, expected_err) 124 | body = self._read_body(int(size)) 125 | return Job(self, int(jid), body, reserved) 126 | 127 | def _interact_yaml(self, command, expected_ok, expected_err=[]): 128 | size, = self._interact(command, expected_ok, expected_err) 129 | body = self._read_body(int(size)) 130 | return self._parse_yaml(body) 131 | 132 | def _interact_peek(self, command): 133 | try: 134 | return self._interact_job(command, ['FOUND'], ['NOT_FOUND'], False) 135 | except CommandFailed: 136 | return None 137 | 138 | # -- public interface -- 139 | 140 | def put(self, body, priority=DEFAULT_PRIORITY, delay=0, ttr=DEFAULT_TTR): 141 | """Put a job into the current tube. Returns job id.""" 142 | assert isinstance(body, str), 'Job body must be a str instance' 143 | jid = self._interact_value('put %d %d %d %d\r\n%s\r\n' % ( 144 | priority, delay, ttr, len(body), body), 145 | ['INSERTED'], 146 | ['JOB_TOO_BIG', 'BURIED', 'DRAINING']) 147 | return int(jid) 148 | 149 | def reserve(self, timeout=None): 150 | """Reserve a job from one of the watched tubes, with optional timeout 151 | in seconds. Returns a Job object, or None if the request times out.""" 152 | if timeout is not None: 153 | command = 'reserve-with-timeout %d\r\n' % timeout 154 | else: 155 | command = 'reserve\r\n' 156 | try: 157 | return self._interact_job(command, 158 | ['RESERVED'], 159 | ['DEADLINE_SOON', 'TIMED_OUT']) 160 | except CommandFailed: 161 | exc = sys.exc_info()[1] 162 | _, status, results = exc.args 163 | if status == 'TIMED_OUT': 164 | return None 165 | elif status == 'DEADLINE_SOON': 166 | raise DeadlineSoon(results) 167 | 168 | def kick(self, bound=1): 169 | """Kick at most bound jobs into the ready queue.""" 170 | return int(self._interact_value('kick %d\r\n' % bound, ['KICKED'])) 171 | 172 | def kick_job(self, jid): 173 | """Kick a specific job into the ready queue.""" 174 | self._interact('kick-job %d\r\n' % jid, ['KICKED'], ['NOT_FOUND']) 175 | 176 | def peek(self, jid): 177 | """Peek at a job. Returns a Job, or None.""" 178 | return self._interact_peek('peek %d\r\n' % jid) 179 | 180 | def peek_ready(self): 181 | """Peek at next ready job. Returns a Job, or None.""" 182 | return self._interact_peek('peek-ready\r\n') 183 | 184 | def peek_delayed(self): 185 | """Peek at next delayed job. Returns a Job, or None.""" 186 | return self._interact_peek('peek-delayed\r\n') 187 | 188 | def peek_buried(self): 189 | """Peek at next buried job. Returns a Job, or None.""" 190 | return self._interact_peek('peek-buried\r\n') 191 | 192 | def tubes(self): 193 | """Return a list of all existing tubes.""" 194 | return self._interact_yaml('list-tubes\r\n', ['OK']) 195 | 196 | def using(self): 197 | """Return the tube currently being used.""" 198 | return self._interact_value('list-tube-used\r\n', ['USING']) 199 | 200 | def use(self, name): 201 | """Use a given tube.""" 202 | return self._interact_value('use %s\r\n' % name, ['USING']) 203 | 204 | def watching(self): 205 | """Return a list of all tubes being watched.""" 206 | return self._interact_yaml('list-tubes-watched\r\n', ['OK']) 207 | 208 | def watch(self, name): 209 | """Watch a given tube.""" 210 | return int(self._interact_value('watch %s\r\n' % name, ['WATCHING'])) 211 | 212 | def ignore(self, name): 213 | """Stop watching a given tube.""" 214 | try: 215 | return int(self._interact_value('ignore %s\r\n' % name, 216 | ['WATCHING'], 217 | ['NOT_IGNORED'])) 218 | except CommandFailed: 219 | # Tried to ignore the only tube in the watchlist, which failed. 220 | return 0 221 | 222 | def stats(self): 223 | """Return a dict of beanstalkd statistics.""" 224 | return self._interact_yaml('stats\r\n', ['OK']) 225 | 226 | def stats_tube(self, name): 227 | """Return a dict of stats about a given tube.""" 228 | return self._interact_yaml('stats-tube %s\r\n' % name, 229 | ['OK'], 230 | ['NOT_FOUND']) 231 | 232 | def pause_tube(self, name, delay): 233 | """Pause a tube for a given delay time, in seconds.""" 234 | self._interact('pause-tube %s %d\r\n' % (name, delay), 235 | ['PAUSED'], 236 | ['NOT_FOUND']) 237 | 238 | # -- job interactors -- 239 | 240 | def delete(self, jid): 241 | """Delete a job, by job id.""" 242 | self._interact('delete %d\r\n' % jid, ['DELETED'], ['NOT_FOUND']) 243 | 244 | def release(self, jid, priority=DEFAULT_PRIORITY, delay=0): 245 | """Release a reserved job back into the ready queue.""" 246 | self._interact('release %d %d %d\r\n' % (jid, priority, delay), 247 | ['RELEASED', 'BURIED'], 248 | ['NOT_FOUND']) 249 | 250 | def bury(self, jid, priority=DEFAULT_PRIORITY): 251 | """Bury a job, by job id.""" 252 | self._interact('bury %d %d\r\n' % (jid, priority), 253 | ['BURIED'], 254 | ['NOT_FOUND']) 255 | 256 | def touch(self, jid): 257 | """Touch a job, by job id, requesting more time to work on a reserved 258 | job before it expires.""" 259 | self._interact('touch %d\r\n' % jid, ['TOUCHED'], ['NOT_FOUND']) 260 | 261 | def stats_job(self, jid): 262 | """Return a dict of stats about a job, by job id.""" 263 | return self._interact_yaml('stats-job %d\r\n' % jid, 264 | ['OK'], 265 | ['NOT_FOUND']) 266 | 267 | 268 | class Job(object): 269 | def __init__(self, conn, jid, body, reserved=True): 270 | self.conn = conn 271 | self.jid = jid 272 | self.body = body 273 | self.reserved = reserved 274 | 275 | def _priority(self): 276 | stats = self.stats() 277 | if isinstance(stats, dict): 278 | return stats['pri'] 279 | return DEFAULT_PRIORITY 280 | 281 | # -- public interface -- 282 | 283 | def delete(self): 284 | """Delete this job.""" 285 | self.conn.delete(self.jid) 286 | self.reserved = False 287 | 288 | def release(self, priority=None, delay=0): 289 | """Release this job back into the ready queue.""" 290 | if self.reserved: 291 | self.conn.release(self.jid, priority or self._priority(), delay) 292 | self.reserved = False 293 | 294 | def bury(self, priority=None): 295 | """Bury this job.""" 296 | if self.reserved: 297 | self.conn.bury(self.jid, priority or self._priority()) 298 | self.reserved = False 299 | 300 | def kick(self): 301 | """Kick this job alive.""" 302 | self.conn.kick_job(self.jid) 303 | 304 | def touch(self): 305 | """Touch this reserved job, requesting more time to work on it before 306 | it expires.""" 307 | if self.reserved: 308 | self.conn.touch(self.jid) 309 | 310 | def stats(self): 311 | """Return a dict of stats about this job.""" 312 | return self.conn.stats_job(self.jid) 313 | 314 | 315 | if __name__ == '__main__': 316 | import nose 317 | nose.main(argv=['nosetests', '-c', '.nose.cfg']) 318 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from setuptools import setup 4 | 5 | from beanstalkc import __version__ as src_version 6 | 7 | PKG_VERSION = os.environ.get('BEANSTALKC_PKG_VERSION', src_version) 8 | 9 | setup( 10 | name='beanstalkc', 11 | version=PKG_VERSION, 12 | py_modules=['beanstalkc'], 13 | 14 | author='Andreas Bolka', 15 | author_email='a@bolka.at', 16 | description='A simple beanstalkd client library', 17 | long_description=''' 18 | beanstalkc is a simple beanstalkd client library for Python. `beanstalkd 19 | `_ is a fast, distributed, in-memory 20 | workqueue service. 21 | ''', 22 | url='http://github.com/earl/beanstalkc', 23 | license='Apache License, Version 2.0', 24 | classifiers=[ 25 | 'Development Status :: 4 - Beta', 26 | 'Intended Audience :: Developers', 27 | 'License :: OSI Approved :: Apache Software License', 28 | 'Programming Language :: Python', 29 | 'Operating System :: OS Independent', 30 | 'Topic :: Software Development :: Libraries :: Python Modules', 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/earl/beanstalkc/70c2ffc41cc84b0a1ae557e470e1db89b7b61023/test/__init__.py -------------------------------------------------------------------------------- /test/fixtures.py: -------------------------------------------------------------------------------- 1 | import os, signal, time 2 | 3 | _BEANSTALKD_PID = None 4 | 5 | def setup(module): 6 | beanstalkd = os.getenv('BEANSTALKD', 'beanstalkd') 7 | module._BEANSTALKD_PID = os.spawnlp( 8 | os.P_NOWAIT, 9 | beanstalkd, 10 | beanstalkd, '-l', '127.0.0.1', '-p', '14711') 11 | time.sleep(0.5) # Give beanstalkd some time to ready. 12 | 13 | def teardown(module): 14 | os.kill(module._BEANSTALKD_PID, signal.SIGTERM) 15 | -------------------------------------------------------------------------------- /test/no-yaml.mkd: -------------------------------------------------------------------------------- 1 | Regression tests for YAMLless operation 2 | ======================================= 3 | 4 | Job priorities are not preserved 5 | -------------------------------- 6 | 7 | Setup a connection that won't parse YAML: 8 | 9 | >>> import beanstalkc 10 | >>> beanstalk = beanstalkc.Connection(host='localhost', port=14711, 11 | ... parse_yaml=False) 12 | 13 | Observe that YAML is not parsed: 14 | 15 | >>> not isinstance(beanstalk.stats(), dict) 16 | True 17 | 18 | Note that while Job#release and Job#bury will still work, they won't 19 | automatically maintain the released/buried Job's priority: 20 | 21 | >>> jid = beanstalk.put('foo', priority=42) 22 | 23 | >>> job = beanstalk.reserve() 24 | >>> print(repr(job.stats())) # doctest: +ELLIPSIS 25 | '...pri: 42...' 26 | 27 | >>> job.release() # Succeeds, but ... 28 | 29 | >>> job = beanstalk.reserve() # ... may not do what you want. 30 | >>> print(repr(job.stats())) # doctest: +ELLIPSIS 31 | '...pri: 2147483648...' 32 | 33 | >>> job.delete() 34 | 35 | Same for Job#bury: 36 | 37 | >>> jid = beanstalk.put('bar', priority=42) 38 | 39 | >>> job = beanstalk.reserve() 40 | >>> print(repr(job.stats())) # doctest: +ELLIPSIS 41 | '...pri: 42...' 42 | 43 | >>> job.bury() 44 | 45 | >>> print(repr(beanstalk.stats_job(jid))) # doctest: +ELLIPSIS 46 | '...pri: 2147483648...' 47 | 48 | And that was that. 49 | 50 | >>> beanstalk.close() 51 | -------------------------------------------------------------------------------- /test/no-yaml_fixtures.py: -------------------------------------------------------------------------------- 1 | from fixtures import setup, teardown 2 | --------------------------------------------------------------------------------