Merge lp:~eday/burrow/prototype-conversion into lp:burrow

Proposed by Eric Day
Status: Merged
Approved by: Eric Day
Approved revision: 2
Merged at revision: 2
Proposed branch: lp:~eday/burrow/prototype-conversion
Merge into: lp:burrow
Diff against target: 1874 lines (+1780/-0)
17 files modified
Authors (+1/-0)
ChangeLog (+4/-0)
LICENSE (+202/-0)
MANIFEST.in (+4/-0)
bin/burrow (+18/-0)
bin/burrowd (+34/-0)
burrow/__init__.py (+19/-0)
burrowd/__init__.py (+131/-0)
burrowd/backend/__init__.py (+94/-0)
burrowd/backend/memory.py (+176/-0)
burrowd/backend/sqlite.py (+250/-0)
burrowd/config.py (+55/-0)
burrowd/frontend/__init__.py (+30/-0)
burrowd/frontend/wsgi.py (+276/-0)
etc/burrowd.conf (+99/-0)
setup.py (+74/-0)
test/frontend/test_wsgi.py (+313/-0)
To merge this branch: bzr merge lp:~eday/burrow/prototype-conversion
Reviewer Review Type Date Requested Status
Burrow Core Team Pending
Review via email: mp+53928@code.launchpad.net

Description of the change

Python prototype conversion to get new trunk started.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'Authors'
2--- Authors 1970-01-01 00:00:00 +0000
3+++ Authors 2011-03-17 23:47:24 +0000
4@@ -0,0 +1,1 @@
5+Eric Day <eday@oddments.org>
6
7=== added file 'ChangeLog'
8--- ChangeLog 1970-01-01 00:00:00 +0000
9+++ ChangeLog 2011-03-17 23:47:24 +0000
10@@ -0,0 +1,4 @@
11+2011-03-17 Eric Day <eday@oddments.org>
12+
13+ Created new burrow trunk.
14+
15
16=== added file 'LICENSE'
17--- LICENSE 1970-01-01 00:00:00 +0000
18+++ LICENSE 2011-03-17 23:47:24 +0000
19@@ -0,0 +1,202 @@
20+
21+ Apache License
22+ Version 2.0, January 2004
23+ http://www.apache.org/licenses/
24+
25+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
26+
27+ 1. Definitions.
28+
29+ "License" shall mean the terms and conditions for use, reproduction,
30+ and distribution as defined by Sections 1 through 9 of this document.
31+
32+ "Licensor" shall mean the copyright owner or entity authorized by
33+ the copyright owner that is granting the License.
34+
35+ "Legal Entity" shall mean the union of the acting entity and all
36+ other entities that control, are controlled by, or are under common
37+ control with that entity. For the purposes of this definition,
38+ "control" means (i) the power, direct or indirect, to cause the
39+ direction or management of such entity, whether by contract or
40+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
41+ outstanding shares, or (iii) beneficial ownership of such entity.
42+
43+ "You" (or "Your") shall mean an individual or Legal Entity
44+ exercising permissions granted by this License.
45+
46+ "Source" form shall mean the preferred form for making modifications,
47+ including but not limited to software source code, documentation
48+ source, and configuration files.
49+
50+ "Object" form shall mean any form resulting from mechanical
51+ transformation or translation of a Source form, including but
52+ not limited to compiled object code, generated documentation,
53+ and conversions to other media types.
54+
55+ "Work" shall mean the work of authorship, whether in Source or
56+ Object form, made available under the License, as indicated by a
57+ copyright notice that is included in or attached to the work
58+ (an example is provided in the Appendix below).
59+
60+ "Derivative Works" shall mean any work, whether in Source or Object
61+ form, that is based on (or derived from) the Work and for which the
62+ editorial revisions, annotations, elaborations, or other modifications
63+ represent, as a whole, an original work of authorship. For the purposes
64+ of this License, Derivative Works shall not include works that remain
65+ separable from, or merely link (or bind by name) to the interfaces of,
66+ the Work and Derivative Works thereof.
67+
68+ "Contribution" shall mean any work of authorship, including
69+ the original version of the Work and any modifications or additions
70+ to that Work or Derivative Works thereof, that is intentionally
71+ submitted to Licensor for inclusion in the Work by the copyright owner
72+ or by an individual or Legal Entity authorized to submit on behalf of
73+ the copyright owner. For the purposes of this definition, "submitted"
74+ means any form of electronic, verbal, or written communication sent
75+ to the Licensor or its representatives, including but not limited to
76+ communication on electronic mailing lists, source code control systems,
77+ and issue tracking systems that are managed by, or on behalf of, the
78+ Licensor for the purpose of discussing and improving the Work, but
79+ excluding communication that is conspicuously marked or otherwise
80+ designated in writing by the copyright owner as "Not a Contribution."
81+
82+ "Contributor" shall mean Licensor and any individual or Legal Entity
83+ on behalf of whom a Contribution has been received by Licensor and
84+ subsequently incorporated within the Work.
85+
86+ 2. Grant of Copyright License. Subject to the terms and conditions of
87+ this License, each Contributor hereby grants to You a perpetual,
88+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
89+ copyright license to reproduce, prepare Derivative Works of,
90+ publicly display, publicly perform, sublicense, and distribute the
91+ Work and such Derivative Works in Source or Object form.
92+
93+ 3. Grant of Patent License. Subject to the terms and conditions of
94+ this License, each Contributor hereby grants to You a perpetual,
95+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
96+ (except as stated in this section) patent license to make, have made,
97+ use, offer to sell, sell, import, and otherwise transfer the Work,
98+ where such license applies only to those patent claims licensable
99+ by such Contributor that are necessarily infringed by their
100+ Contribution(s) alone or by combination of their Contribution(s)
101+ with the Work to which such Contribution(s) was submitted. If You
102+ institute patent litigation against any entity (including a
103+ cross-claim or counterclaim in a lawsuit) alleging that the Work
104+ or a Contribution incorporated within the Work constitutes direct
105+ or contributory patent infringement, then any patent licenses
106+ granted to You under this License for that Work shall terminate
107+ as of the date such litigation is filed.
108+
109+ 4. Redistribution. You may reproduce and distribute copies of the
110+ Work or Derivative Works thereof in any medium, with or without
111+ modifications, and in Source or Object form, provided that You
112+ meet the following conditions:
113+
114+ (a) You must give any other recipients of the Work or
115+ Derivative Works a copy of this License; and
116+
117+ (b) You must cause any modified files to carry prominent notices
118+ stating that You changed the files; and
119+
120+ (c) You must retain, in the Source form of any Derivative Works
121+ that You distribute, all copyright, patent, trademark, and
122+ attribution notices from the Source form of the Work,
123+ excluding those notices that do not pertain to any part of
124+ the Derivative Works; and
125+
126+ (d) If the Work includes a "NOTICE" text file as part of its
127+ distribution, then any Derivative Works that You distribute must
128+ include a readable copy of the attribution notices contained
129+ within such NOTICE file, excluding those notices that do not
130+ pertain to any part of the Derivative Works, in at least one
131+ of the following places: within a NOTICE text file distributed
132+ as part of the Derivative Works; within the Source form or
133+ documentation, if provided along with the Derivative Works; or,
134+ within a display generated by the Derivative Works, if and
135+ wherever such third-party notices normally appear. The contents
136+ of the NOTICE file are for informational purposes only and
137+ do not modify the License. You may add Your own attribution
138+ notices within Derivative Works that You distribute, alongside
139+ or as an addendum to the NOTICE text from the Work, provided
140+ that such additional attribution notices cannot be construed
141+ as modifying the License.
142+
143+ You may add Your own copyright statement to Your modifications and
144+ may provide additional or different license terms and conditions
145+ for use, reproduction, or distribution of Your modifications, or
146+ for any such Derivative Works as a whole, provided Your use,
147+ reproduction, and distribution of the Work otherwise complies with
148+ the conditions stated in this License.
149+
150+ 5. Submission of Contributions. Unless You explicitly state otherwise,
151+ any Contribution intentionally submitted for inclusion in the Work
152+ by You to the Licensor shall be under the terms and conditions of
153+ this License, without any additional terms or conditions.
154+ Notwithstanding the above, nothing herein shall supersede or modify
155+ the terms of any separate license agreement you may have executed
156+ with Licensor regarding such Contributions.
157+
158+ 6. Trademarks. This License does not grant permission to use the trade
159+ names, trademarks, service marks, or product names of the Licensor,
160+ except as required for reasonable and customary use in describing the
161+ origin of the Work and reproducing the content of the NOTICE file.
162+
163+ 7. Disclaimer of Warranty. Unless required by applicable law or
164+ agreed to in writing, Licensor provides the Work (and each
165+ Contributor provides its Contributions) on an "AS IS" BASIS,
166+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
167+ implied, including, without limitation, any warranties or conditions
168+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
169+ PARTICULAR PURPOSE. You are solely responsible for determining the
170+ appropriateness of using or redistributing the Work and assume any
171+ risks associated with Your exercise of permissions under this License.
172+
173+ 8. Limitation of Liability. In no event and under no legal theory,
174+ whether in tort (including negligence), contract, or otherwise,
175+ unless required by applicable law (such as deliberate and grossly
176+ negligent acts) or agreed to in writing, shall any Contributor be
177+ liable to You for damages, including any direct, indirect, special,
178+ incidental, or consequential damages of any character arising as a
179+ result of this License or out of the use or inability to use the
180+ Work (including but not limited to damages for loss of goodwill,
181+ work stoppage, computer failure or malfunction, or any and all
182+ other commercial damages or losses), even if such Contributor
183+ has been advised of the possibility of such damages.
184+
185+ 9. Accepting Warranty or Additional Liability. While redistributing
186+ the Work or Derivative Works thereof, You may choose to offer,
187+ and charge a fee for, acceptance of support, warranty, indemnity,
188+ or other liability obligations and/or rights consistent with this
189+ License. However, in accepting such obligations, You may act only
190+ on Your own behalf and on Your sole responsibility, not on behalf
191+ of any other Contributor, and only if You agree to indemnify,
192+ defend, and hold each Contributor harmless for any liability
193+ incurred by, or claims asserted against, such Contributor by reason
194+ of your accepting any such warranty or additional liability.
195+
196+ END OF TERMS AND CONDITIONS
197+
198+ APPENDIX: How to apply the Apache License to your work.
199+
200+ To apply the Apache License to your work, attach the following
201+ boilerplate notice, with the fields enclosed by brackets "[]"
202+ replaced with your own identifying information. (Don't include
203+ the brackets!) The text should be enclosed in the appropriate
204+ comment syntax for the file format. We also recommend that a
205+ file or class name and description of purpose be included on the
206+ same "printed page" as the copyright notice for easier
207+ identification within third-party archives.
208+
209+ Copyright [yyyy] [name of copyright owner]
210+
211+ Licensed under the Apache License, Version 2.0 (the "License");
212+ you may not use this file except in compliance with the License.
213+ You may obtain a copy of the License at
214+
215+ http://www.apache.org/licenses/LICENSE-2.0
216+
217+ Unless required by applicable law or agreed to in writing, software
218+ distributed under the License is distributed on an "AS IS" BASIS,
219+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
220+ See the License for the specific language governing permissions and
221+ limitations under the License.
222
223=== added file 'MANIFEST.in'
224--- MANIFEST.in 1970-01-01 00:00:00 +0000
225+++ MANIFEST.in 2011-03-17 23:47:24 +0000
226@@ -0,0 +1,4 @@
227+graft etc
228+include Authors
229+include ChangeLog
230+include LICENSE
231
232=== added directory 'bin'
233=== added file 'bin/burrow'
234--- bin/burrow 1970-01-01 00:00:00 +0000
235+++ bin/burrow 2011-03-17 23:47:24 +0000
236@@ -0,0 +1,18 @@
237+#!/usr/bin/env python
238+# Copyright (C) 2011 OpenStack LLC.
239+#
240+# Licensed under the Apache License, Version 2.0 (the "License");
241+# you may not use this file except in compliance with the License.
242+# You may obtain a copy of the License at
243+#
244+# http://www.apache.org/licenses/LICENSE-2.0
245+#
246+# Unless required by applicable law or agreed to in writing, software
247+# distributed under the License is distributed on an "AS IS" BASIS,
248+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
249+# See the License for the specific language governing permissions and
250+# limitations under the License.
251+
252+'''
253+Burrow command line client.
254+'''
255
256=== added file 'bin/burrowd'
257--- bin/burrowd 1970-01-01 00:00:00 +0000
258+++ bin/burrowd 2011-03-17 23:47:24 +0000
259@@ -0,0 +1,34 @@
260+#!/usr/bin/env python
261+# Copyright (C) 2011 OpenStack LLC.
262+#
263+# Licensed under the Apache License, Version 2.0 (the "License");
264+# you may not use this file except in compliance with the License.
265+# You may obtain a copy of the License at
266+#
267+# http://www.apache.org/licenses/LICENSE-2.0
268+#
269+# Unless required by applicable law or agreed to in writing, software
270+# distributed under the License is distributed on an "AS IS" BASIS,
271+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
272+# See the License for the specific language governing permissions and
273+# limitations under the License.
274+
275+'''
276+Burrow server.
277+'''
278+
279+import os
280+import sys
281+
282+# If ../burrowd/__init__.py exists, add ../ to the Python search path so
283+# that it will override whatever may be installed in the default Python
284+# search path.
285+BASE_DIRECTORY = os.path.join(os.path.abspath(__file__), os.pardir, os.pardir)
286+BASE_DIRECTORY = os.path.normpath(BASE_DIRECTORY)
287+if os.path.exists(os.path.join(BASE_DIRECTORY, 'burrowd', '__init__.py')):
288+ sys.path.insert(0, BASE_DIRECTORY)
289+
290+import burrowd
291+
292+if __name__ == '__main__':
293+ burrowd.Burrowd(sys.argv[1:]).run()
294
295=== added directory 'burrow'
296=== added file 'burrow/__init__.py'
297--- burrow/__init__.py 1970-01-01 00:00:00 +0000
298+++ burrow/__init__.py 2011-03-17 23:47:24 +0000
299@@ -0,0 +1,19 @@
300+# Copyright (C) 2011 OpenStack LLC.
301+#
302+# Licensed under the Apache License, Version 2.0 (the "License");
303+# you may not use this file except in compliance with the License.
304+# You may obtain a copy of the License at
305+#
306+# http://www.apache.org/licenses/LICENSE-2.0
307+#
308+# Unless required by applicable law or agreed to in writing, software
309+# distributed under the License is distributed on an "AS IS" BASIS,
310+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
311+# See the License for the specific language governing permissions and
312+# limitations under the License.
313+
314+'''Main client module for burrow.'''
315+
316+
317+class Burrow(object):
318+ pass
319
320=== added directory 'burrowd'
321=== added file 'burrowd/__init__.py'
322--- burrowd/__init__.py 1970-01-01 00:00:00 +0000
323+++ burrowd/__init__.py 2011-03-17 23:47:24 +0000
324@@ -0,0 +1,131 @@
325+# Copyright (C) 2011 OpenStack LLC.
326+#
327+# Licensed under the Apache License, Version 2.0 (the "License");
328+# you may not use this file except in compliance with the License.
329+# You may obtain a copy of the License at
330+#
331+# http://www.apache.org/licenses/LICENSE-2.0
332+#
333+# Unless required by applicable law or agreed to in writing, software
334+# distributed under the License is distributed on an "AS IS" BASIS,
335+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
336+# See the License for the specific language governing permissions and
337+# limitations under the License.
338+
339+'''Main server module for burrow.'''
340+
341+import ConfigParser
342+import gettext
343+import logging
344+import logging.config
345+import sys
346+
347+import eventlet
348+
349+import burrowd.config
350+
351+# This installs the _(...) function as a built-in so all other modules
352+# don't need to.
353+gettext.install('burrowd')
354+
355+# Default configuration values for this module.
356+DEFAULT_BACKEND = 'burrowd.backend.sqlite'
357+DEFAULT_FRONTENDS = 'burrowd.frontend.wsgi'
358+DEFAULT_THREAD_POOL_SIZE = 1000
359+
360+
361+class Burrowd(object):
362+ '''Server class for burrow.'''
363+
364+ def __init__(self, config_files=[], add_default_log_handler=True):
365+ '''Initialize a server using the config files from the given
366+ list. This is passed directly to ConfigParser.read(), so
367+ files should be in ConfigParser format. This will load all
368+ frontend and backend classes from the configuration.'''
369+ if len(config_files) > 0:
370+ logging.config.fileConfig(config_files)
371+ self._config = ConfigParser.ConfigParser()
372+ self._config.read(config_files)
373+ self.config = burrowd.config.Config(self._config, 'burrowd')
374+ self.log = get_logger(self.config)
375+ if add_default_log_handler:
376+ self._add_default_log_handler()
377+ self._import_backend()
378+ self._import_frontends()
379+
380+ def _add_default_log_handler(self):
381+ '''Add a default log handler it one has not been set.'''
382+ root_log = logging.getLogger()
383+ if len(root_log.handlers) > 0 or len(self.log.handlers) > 0:
384+ return
385+ handler = logging.StreamHandler()
386+ handler.setLevel(logging.DEBUG)
387+ log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
388+ handler.setFormatter(logging.Formatter(log_format))
389+ root_log.addHandler(handler)
390+
391+ def _import_backend(self):
392+ '''Load backend given in the 'backend' option.'''
393+ backend = self.config.get('backend', DEFAULT_BACKEND)
394+ config = (self._config, backend)
395+ self.backend = import_class(backend, 'Backend')(config)
396+
397+ def _import_frontends(self):
398+ '''Load frontends given in the 'frontends' option.'''
399+ self.frontends = []
400+ frontends = self.config.get('frontends', DEFAULT_FRONTENDS)
401+ for frontend in frontends.split(','):
402+ frontend = frontend.split(':')
403+ if len(frontend) == 1:
404+ frontend.append(None)
405+ config = (self._config, frontend[0], frontend[1])
406+ frontend = import_class(frontend[0], 'Frontend')
407+ frontend = frontend(config, self.backend)
408+ self.frontends.append(frontend)
409+
410+ def run(self):
411+ '''Create the thread pool and start the main server loop. Wait
412+ for the pool to complete, but possibly run forever if the
413+ frontends and backend never remove threads.'''
414+ thread_pool_size = self.config.getint('thread_pool_size',
415+ DEFAULT_THREAD_POOL_SIZE)
416+ thread_pool = eventlet.GreenPool(size=int(thread_pool_size))
417+ self.backend.run(thread_pool)
418+ for frontend in self.frontends:
419+ frontend.run(thread_pool)
420+ self.log.info(_('Waiting for all threads to exit'))
421+ try:
422+ thread_pool.waitall()
423+ except KeyboardInterrupt:
424+ pass
425+
426+
427+class Module(object):
428+ '''Common module class for burrow.'''
429+
430+ def __init__(self, config):
431+ self.config = burrowd.config.Config(*config)
432+ self.log = get_logger(self.config)
433+ self.log.debug(_('Module created'))
434+
435+
436+def get_logger(config):
437+ '''Create a logger from the given config.'''
438+ log = logging.getLogger(config.section)
439+ log_level = config.get('log_level', 'DEBUG')
440+ log_level = logging.getLevelName(log_level)
441+ if isinstance(log_level, int):
442+ log.setLevel(log_level)
443+ return log
444+
445+
446+def import_class(module_name, class_name=None):
447+ '''Import a class given a full module.class name.'''
448+ if class_name is None:
449+ module_name, _separator, class_name = module_name.rpartition('.')
450+ try:
451+ __import__(module_name)
452+ return getattr(sys.modules[module_name], class_name)
453+ except (ImportError, ValueError, AttributeError), exception:
454+ raise ImportError(_('Class %s.%s cannot be found (%s)') %
455+ (module_name, class_name, exception))
456
457=== added directory 'burrowd/backend'
458=== added file 'burrowd/backend/__init__.py'
459--- burrowd/backend/__init__.py 1970-01-01 00:00:00 +0000
460+++ burrowd/backend/__init__.py 2011-03-17 23:47:24 +0000
461@@ -0,0 +1,94 @@
462+# Copyright (C) 2011 OpenStack LLC.
463+#
464+# Licensed under the Apache License, Version 2.0 (the "License");
465+# you may not use this file except in compliance with the License.
466+# You may obtain a copy of the License at
467+#
468+# http://www.apache.org/licenses/LICENSE-2.0
469+#
470+# Unless required by applicable law or agreed to in writing, software
471+# distributed under the License is distributed on an "AS IS" BASIS,
472+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
473+# See the License for the specific language governing permissions and
474+# limitations under the License.
475+
476+'''Backends for the burrow server.'''
477+
478+import eventlet
479+
480+import burrowd
481+
482+
483+class Backend(burrowd.Module):
484+ '''Interface that backend implementations must provide.'''
485+
486+ def __init__(self, config):
487+ super(Backend, self).__init__(config)
488+ self.queues = {}
489+
490+ def run(self, thread_pool):
491+ thread_pool.spawn_n(self._clean)
492+
493+ def _clean(self):
494+ while True:
495+ self.clean()
496+ eventlet.sleep(1)
497+
498+ def delete_accounts(self):
499+ pass
500+
501+ def get_accounts(self):
502+ return []
503+
504+ def delete_account(self, account):
505+ pass
506+
507+ def get_queues(self, account):
508+ return []
509+
510+ def queue_exists(self, account, queue):
511+ return False
512+
513+ def delete_messages(self, account, queue, limit, marker, match_hidden):
514+ return []
515+
516+ def get_messages(self, account, queue, limit, marker, match_hidden):
517+ return []
518+
519+ def update_messages(self, account, queue, limit, marker, match_hidden, ttl,
520+ hide):
521+ return []
522+
523+ def delete_message(self, account, queue, message_id):
524+ return None
525+
526+ def get_message(self, account, queue, message_id):
527+ return None
528+
529+ def put_message(self, account, queue, message_id, ttl, hide, body):
530+ return True
531+
532+ def update_message(self, account, queue, message_id, ttl, hide):
533+ return None
534+
535+ def clean(self):
536+ '''This method should remove all messages with an expired
537+ TTL and make hidden messages that have an expired hide time
538+ visible again.'''
539+ pass
540+
541+ def notify(self, account, queue):
542+ queue = '%s/%s' % (account, queue)
543+ if queue in self.queues:
544+ self.queues[queue].put(0)
545+
546+ def wait(self, account, queue, seconds):
547+ queue = '%s/%s' % (account, queue)
548+ if queue not in self.queues:
549+ self.queues[queue] = eventlet.Queue()
550+ try:
551+ self.queues[queue].get(timeout=seconds)
552+ except Exception:
553+ pass
554+ if self.queues[queue].getting() == 0:
555+ del self.queues[queue]
556
557=== added file 'burrowd/backend/memory.py'
558--- burrowd/backend/memory.py 1970-01-01 00:00:00 +0000
559+++ burrowd/backend/memory.py 2011-03-17 23:47:24 +0000
560@@ -0,0 +1,176 @@
561+# Copyright (C) 2011 OpenStack LLC.
562+#
563+# Licensed under the Apache License, Version 2.0 (the "License");
564+# you may not use this file except in compliance with the License.
565+# You may obtain a copy of the License at
566+#
567+# http://www.apache.org/licenses/LICENSE-2.0
568+#
569+# Unless required by applicable law or agreed to in writing, software
570+# distributed under the License is distributed on an "AS IS" BASIS,
571+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
572+# See the License for the specific language governing permissions and
573+# limitations under the License.
574+
575+'''Memory backend for the burrow server.'''
576+
577+import time
578+
579+import burrowd.backend
580+
581+
582+class Backend(burrowd.backend.Backend):
583+
584+ def __init__(self, config):
585+ super(Backend, self).__init__(config)
586+ self.accounts = {}
587+
588+ def delete_accounts(self):
589+ self.accounts.clear()
590+
591+ def get_accounts(self):
592+ return self.accounts.keys()
593+
594+ def delete_account(self, account):
595+ del self.accounts[account]
596+
597+ def get_queues(self, account):
598+ if account not in self.accounts:
599+ return []
600+ return self.accounts[account].keys()
601+
602+ def queue_exists(self, account, queue):
603+ return account in self.accounts and queue in self.accounts[account]
604+
605+ def delete_messages(self, account, queue, limit, marker, match_hidden):
606+ messages = self._scan_queue(account, queue, limit, marker,
607+ match_hidden, delete=True)
608+ if len(self.accounts[account][queue]) == 0:
609+ del self.accounts[account][queue]
610+ if len(self.accounts[account]) == 0:
611+ del self.accounts[account]
612+ return messages
613+
614+ def get_messages(self, account, queue, limit, marker, match_hidden):
615+ return self._scan_queue(account, queue, limit, marker, match_hidden)
616+
617+ def update_messages(self, account, queue, limit, marker, match_hidden, ttl,
618+ hide):
619+ return self._scan_queue(account, queue, limit, marker, match_hidden,
620+ ttl=ttl, hide=hide)
621+
622+ def delete_message(self, account, queue, message_id):
623+ for index in range(0, len(self.accounts[account][queue])):
624+ message = self.accounts[account][queue][index]
625+ if message['id'] == message_id:
626+ del self.accounts[account][queue][index]
627+ if len(self.accounts[account][queue]) == 0:
628+ del self.accounts[account][queue]
629+ if len(self.accounts[account]) == 0:
630+ del self.accounts[account]
631+ return message
632+ return None
633+
634+ def get_message(self, account, queue, message_id):
635+ for index in range(0, len(self.accounts[account][queue])):
636+ message = self.accounts[account][queue][index]
637+ if message['id'] == message_id:
638+ return message
639+ return None
640+
641+ def put_message(self, account, queue, message_id, ttl, hide, body):
642+ if account not in self.accounts:
643+ self.accounts[account] = {}
644+ if queue not in self.accounts[account]:
645+ self.accounts[account][queue] = []
646+ for index in range(0, len(self.accounts[account][queue])):
647+ message = self.accounts[account][queue][index]
648+ if message['id'] == message_id:
649+ message['ttl'] = ttl
650+ message['hide'] = hide
651+ message['body'] = body
652+ if hide == 0:
653+ self.notify(account, queue)
654+ return False
655+ message = dict(id=message_id, ttl=ttl, hide=hide, body=body)
656+ self.accounts[account][queue].append(message)
657+ self.notify(account, queue)
658+ return True
659+
660+ def update_message(self, account, queue, message_id, ttl, hide):
661+ for index in range(0, len(self.accounts[account][queue])):
662+ message = self.accounts[account][queue][index]
663+ if message['id'] == message_id:
664+ if ttl is not None:
665+ message['ttl'] = ttl
666+ if hide is not None:
667+ message['hide'] = hide
668+ if hide == 0:
669+ self.notify(account, queue)
670+ return message
671+ return None
672+
673+ def clean(self):
674+ now = int(time.time())
675+ for account in self.accounts.keys():
676+ for queue in self.accounts[account].keys():
677+ index = 0
678+ notify = False
679+ total = len(self.accounts[account][queue])
680+ while index < total:
681+ message = self.accounts[account][queue][index]
682+ if 0 < message['ttl'] <= now:
683+ del self.accounts[account][queue][index]
684+ total -= 1
685+ else:
686+ if 0 < message['hide'] <= now:
687+ message['hide'] = 0
688+ notify = True
689+ index += 1
690+ if notify:
691+ self.notify(account, queue)
692+ if len(self.accounts[account][queue]) == 0:
693+ del self.accounts[account][queue]
694+ if len(self.accounts[account]) == 0:
695+ del self.accounts[account]
696+
697+ def _scan_queue(self, account, queue, limit, marker, match_hidden,
698+ ttl=None, hide=None, delete=False):
699+ index = 0
700+ notify = False
701+ if marker is not None:
702+ found = False
703+ for index in range(0, len(self.accounts[account][queue])):
704+ message = self.accounts[account][queue][index]
705+ if message['id'] == marker:
706+ index += 1
707+ found = True
708+ break
709+ if not found:
710+ index = 0
711+ messages = []
712+ total = len(self.accounts[account][queue])
713+ while index < total:
714+ message = self.accounts[account][queue][index]
715+ if not match_hidden and message['hide'] != 0:
716+ index += 1
717+ continue
718+ if ttl is not None:
719+ message['ttl'] = ttl
720+ if hide is not None:
721+ message['hide'] = hide
722+ if hide == 0:
723+ notify = True
724+ if delete:
725+ del self.accounts[account][queue][index]
726+ total -= 1
727+ else:
728+ index += 1
729+ messages.append(message)
730+ if limit:
731+ limit -= 1
732+ if limit == 0:
733+ break
734+ if notify:
735+ self.notify(account, queue)
736+ return messages
737
738=== added file 'burrowd/backend/sqlite.py'
739--- burrowd/backend/sqlite.py 1970-01-01 00:00:00 +0000
740+++ burrowd/backend/sqlite.py 2011-03-17 23:47:24 +0000
741@@ -0,0 +1,250 @@
742+# Copyright (C) 2011 OpenStack LLC.
743+#
744+# Licensed under the Apache License, Version 2.0 (the "License");
745+# you may not use this file except in compliance with the License.
746+# You may obtain a copy of the License at
747+#
748+# http://www.apache.org/licenses/LICENSE-2.0
749+#
750+# Unless required by applicable law or agreed to in writing, software
751+# distributed under the License is distributed on an "AS IS" BASIS,
752+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
753+# See the License for the specific language governing permissions and
754+# limitations under the License.
755+
756+'''Memory backend for the burrow server.'''
757+
758+import sqlite3
759+import time
760+
761+import burrowd.backend
762+
763+# Default configuration values for this module.
764+DEFAULT_DATABASE = ':memory:'
765+
766+
767+class Backend(burrowd.backend.Backend):
768+
769+ def __init__(self, config):
770+ super(Backend, self).__init__(config)
771+ database = self.config.get('database', DEFAULT_DATABASE)
772+ self.db = sqlite3.connect(database)
773+ queries = [
774+ 'CREATE TABLE queues ('
775+ 'account VARCHAR(255) NOT NULL,'
776+ 'queue VARCHAR(255) NOT NULL,'
777+ 'PRIMARY KEY (account, queue))',
778+ 'CREATE TABLE messages ('
779+ 'queue INT UNSIGNED NOT NULL,'
780+ 'name VARCHAR(255) NOT NULL,'
781+ 'ttl INT UNSIGNED NOT NULL,'
782+ 'hide INT UNSIGNED NOT NULL,'
783+ 'body BLOB NOT NULL,'
784+ 'PRIMARY KEY (queue, name))']
785+ for query in queries:
786+ self.db.execute(query)
787+
788+ def delete_accounts(self):
789+ self.db.execute("DELETE FROM queues")
790+ self.db.execute("DELETE FROM messages")
791+
792+ def get_accounts(self):
793+ result = self.db.execute("SELECT account FROM queues").fetchall()
794+ return [row[0] for row in result]
795+
796+ def delete_account(self, account):
797+ query = "SELECT rowid FROM queues WHERE account='%s'" % account
798+ result = self.db.execute(query).fetchall()
799+ if len(result) == 0:
800+ return
801+ queues = [str(queue[0]) for queue in result]
802+ query = "DELETE FROM messages WHERE queue IN (%s)" % (','.join(queues))
803+ self.db.execute(query)
804+ self.db.execute("DELETE FROM queues WHERE account='%s'" % account)
805+
806+ def get_queues(self, account):
807+ query = "SELECT queue FROM queues WHERE account='%s'" % account
808+ result = self.db.execute(query).fetchall()
809+ return [row[0] for row in result]
810+
811+ def queue_exists(self, account, queue):
812+ query = "SELECT COUNT(*) FROM queues " \
813+ "WHERE account='%s' AND queue='%s'" % \
814+ (account, queue)
815+ result = self.db.execute(query).fetchall()
816+ if len(result) == 0:
817+ return False
818+ self.rowid = result[0][0]
819+ return True
820+
821+ def delete_messages(self, account, queue, limit, marker, match_hidden):
822+ messages = self.get_messages(account, queue, limit, marker,
823+ match_hidden)
824+ ids = [message['id'] for message in messages]
825+ query = "DELETE FROM messages WHERE queue=%d AND name IN (%s)" % \
826+ (self.rowid, ','.join(ids))
827+ self.db.execute(query)
828+ query = "SELECT rowid FROM messages WHERE queue=%d LIMIT 1" % \
829+ self.rowid
830+ if len(self.db.execute(query).fetchall()) == 0:
831+ query = "DELETE FROM queues WHERE rowid=%d" % self.rowid
832+ self.db.execute(query)
833+ return messages
834+
835+ def get_messages(self, account, queue, limit, marker, match_hidden):
836+ if marker is not None:
837+ query = "SELECT rowid FROM messages " \
838+ "WHERE queue=%d AND name='%s'" % \
839+ (self.rowid, marker)
840+ result = self.db.execute(query).fetchall()
841+ if len(result) == 0:
842+ marker = None
843+ else:
844+ marker = result[0][0]
845+ query = "SELECT name,ttl,hide,body FROM messages WHERE queue=%d" % \
846+ self.rowid
847+ if match_hidden is False:
848+ query += " AND hide == 0"
849+ if marker is not None:
850+ query += " AND rowid > %d" % marker
851+ if limit is not None:
852+ query += " LIMIT %d" % limit
853+ result = self.db.execute(query).fetchall()
854+ messages = []
855+ for row in result:
856+ messages.append(dict(id=row[0], ttl=row[1], hide=row[2],
857+ body=row[3]))
858+ return messages
859+
860+ def update_messages(self, account, queue, limit, marker, match_hidden, ttl,
861+ hide):
862+ messages = self.get_messages(account, queue, limit, marker,
863+ match_hidden)
864+ query = "UPDATE messages SET"
865+ comma = ''
866+ if ttl is not None:
867+ query += "%s ttl=%d" % (comma, ttl)
868+ comma = ','
869+ if hide is not None:
870+ query += "%s hide=%d" % (comma, hide)
871+ comma = ','
872+ if comma == '':
873+ return (False, message)
874+ ids = []
875+ for message in messages:
876+ ids.append(message['id'])
877+ if ttl is not None:
878+ message['ttl'] = ttl
879+ if hide is not None:
880+ message['hide'] = hide
881+ query += " WHERE queue=%d AND name IN (%s)" % \
882+ (self.rowid, ','.join(ids))
883+ self.db.execute(query)
884+ self.notify(account, queue)
885+ return messages
886+
887+ def delete_message(self, account, queue, message_id):
888+ message = self.get_message(account, queue, message_id)
889+ if message is None:
890+ return None
891+ query = "DELETE FROM messages WHERE queue=%d AND name='%s'" % \
892+ (self.rowid, message_id)
893+ self.db.execute(query)
894+ query = "SELECT rowid FROM messages WHERE queue=%d LIMIT 1" % \
895+ self.rowid
896+ if len(self.db.execute(query).fetchall()) == 0:
897+ query = "DELETE FROM queues WHERE rowid=%d" % self.rowid
898+ self.db.execute(query)
899+ return message
900+
901+ def get_message(self, account, queue, message_id):
902+ query = "SELECT name,ttl,hide,body FROM messages " \
903+ "WHERE queue=%d AND name='%s'" % (self.rowid, message_id)
904+ result = self.db.execute(query).fetchall()
905+ if len(result) == 0:
906+ return None
907+ row = result[0]
908+ return dict(id=row[0], ttl=row[1], hide=row[2], body=row[3])
909+
910+ def put_message(self, account, queue, message_id, ttl, hide, body):
911+ query = "SELECT rowid FROM queues " \
912+ "WHERE account='%s' AND queue='%s'" % (account, queue)
913+ result = self.db.execute(query).fetchall()
914+ if len(result) == 0:
915+ query = "INSERT INTO queues VALUES ('%s', '%s')" % (account, queue)
916+ rowid = self.db.execute(query).lastrowid
917+ else:
918+ rowid = result[0][0]
919+ query = "SELECT rowid FROM messages WHERE queue=%d AND name='%s'" % \
920+ (rowid, message_id)
921+ result = self.db.execute(query).fetchall()
922+ if len(result) == 0:
923+ query = "INSERT INTO messages VALUES (%d, '%s', %d, %d, '%s')" % \
924+ (rowid, message_id, ttl, hide, body)
925+ self.db.execute(query)
926+ self.notify(account, queue)
927+ return True
928+ query = "UPDATE messages SET ttl=%d, hide=%d, body='%s'" \
929+ "WHERE rowid=%d" % (ttl, hide, body, result[0][0])
930+ self.db.execute(query)
931+ if hide == 0:
932+ self.notify(account, queue)
933+ return False
934+
935+ def update_message(self, account, queue, message_id, ttl, hide):
936+ message = self.get_message(account, queue, message_id)
937+ if message is None:
938+ return None
939+ query = "UPDATE messages SET"
940+ comma = ''
941+ if ttl is not None:
942+ query += "%s ttl=%d" % (comma, ttl)
943+ comma = ','
944+ if hide is not None:
945+ query += "%s hide=%d" % (comma, hide)
946+ comma = ','
947+ if comma == '':
948+ return message
949+ query += " WHERE queue=%d AND name='%s'" % (self.rowid, message_id)
950+ self.db.execute(query)
951+ if hide == 0:
952+ self.notify(account, queue)
953+ return message
954+
955+ def clean(self):
956+ now = int(time.time())
957+ query = "SELECT rowid,queue FROM messages " \
958+ "WHERE ttl > 0 AND ttl <= %d" % now
959+ result = self.db.execute(query).fetchall()
960+ if len(result) > 0:
961+ messages = []
962+ queues = []
963+ for row in result:
964+ messages.append(str(row[0]))
965+ queues.append(row[1])
966+ query = 'DELETE FROM messages WHERE rowid in (%s)' % \
967+ ','.join(messages)
968+ self.db.execute(query)
969+ for queue in queues:
970+ query = "SELECT rowid FROM messages WHERE queue=%d LIMIT 1" % \
971+ queue
972+ if len(self.db.execute(query).fetchall()) == 0:
973+ query = "DELETE FROM queues WHERE rowid=%d" % queue
974+ self.db.execute(query)
975+ query = "SELECT rowid,queue FROM messages WHERE " \
976+ "hide > 0 AND hide <= %d" % now
977+ result = self.db.execute(query).fetchall()
978+ if len(result) > 0:
979+ messages = []
980+ queues = []
981+ for row in result:
982+ messages.append(str(row[0]))
983+ queues.append(row[1])
984+ query = 'UPDATE messages SET hide=0 WHERE rowid in (%s)' % \
985+ ','.join(messages)
986+ self.db.execute(query)
987+ for queue in queues:
988+ query = "SELECT account,queue FROM queues WHERE rowid=%d" % \
989+ queue
990+ result = self.db.execute(query).fetchall()[0]
991+ self.notify(result[0], result[1])
992
993=== added file 'burrowd/config.py'
994--- burrowd/config.py 1970-01-01 00:00:00 +0000
995+++ burrowd/config.py 2011-03-17 23:47:24 +0000
996@@ -0,0 +1,55 @@
997+# Copyright (C) 2011 OpenStack LLC.
998+#
999+# Licensed under the Apache License, Version 2.0 (the "License");
1000+# you may not use this file except in compliance with the License.
1001+# You may obtain a copy of the License at
1002+#
1003+# http://www.apache.org/licenses/LICENSE-2.0
1004+#
1005+# Unless required by applicable law or agreed to in writing, software
1006+# distributed under the License is distributed on an "AS IS" BASIS,
1007+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1008+# See the License for the specific language governing permissions and
1009+# limitations under the License.
1010+
1011+'''Configuration module for the burrow server.'''
1012+
1013+import ConfigParser
1014+
1015+
1016+class Config(object):
1017+ '''Configuration class that wraps the ConfigParser get*
1018+ methods. These wrappers automatically check for options in
1019+ a specific instance section first before the regular section
1020+ (section:instance and then section). They will also return a
1021+ default value if given instead of throwing an exception.'''
1022+
1023+ def __init__(self, config, section, instance=None):
1024+ self.config = config
1025+ self.section = section
1026+ if instance is None:
1027+ self.instance = None
1028+ else:
1029+ self.instance = '%s:%s' % (section, instance)
1030+
1031+ def get(self, option, default=None):
1032+ return self._get(self.config.get, option, default)
1033+
1034+ def getboolean(self, option, default=None):
1035+ return self._get(self.config.getboolean, option, default)
1036+
1037+ def getfloat(self, option, default=None):
1038+ return self._get(self.config.getfloat, option, default)
1039+
1040+ def getint(self, option, default=None):
1041+ return self._get(self.config.getint, option, default)
1042+
1043+ def _get(self, method, option, default):
1044+ if self.instance is not None:
1045+ if self.config.has_option(self.instance, option):
1046+ return method(self.instance, option)
1047+ if self.config.has_option(self.section, option):
1048+ return method(self.section, option)
1049+ if self.config.has_option(ConfigParser.DEFAULTSECT, option):
1050+ return method(ConfigParser.DEFAULTSECT, option)
1051+ return default
1052
1053=== added directory 'burrowd/frontend'
1054=== added file 'burrowd/frontend/__init__.py'
1055--- burrowd/frontend/__init__.py 1970-01-01 00:00:00 +0000
1056+++ burrowd/frontend/__init__.py 2011-03-17 23:47:24 +0000
1057@@ -0,0 +1,30 @@
1058+# Copyright (C) 2011 OpenStack LLC.
1059+#
1060+# Licensed under the Apache License, Version 2.0 (the "License");
1061+# you may not use this file except in compliance with the License.
1062+# You may obtain a copy of the License at
1063+#
1064+# http://www.apache.org/licenses/LICENSE-2.0
1065+#
1066+# Unless required by applicable law or agreed to in writing, software
1067+# distributed under the License is distributed on an "AS IS" BASIS,
1068+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1069+# See the License for the specific language governing permissions and
1070+# limitations under the License.
1071+
1072+'''Frontends for the burrow server.'''
1073+
1074+import burrowd
1075+
1076+
1077+class Frontend(burrowd.Module):
1078+ '''Interface that frontend implementations must provide.'''
1079+
1080+ def __init__(self, config, backend):
1081+ super(Frontend, self).__init__(config)
1082+ self.backend = backend
1083+
1084+ def run(self, thread_pool):
1085+ '''Run the frontend instance, adding any threads to the
1086+ thread_pool if needed.'''
1087+ pass
1088
1089=== added file 'burrowd/frontend/wsgi.py'
1090--- burrowd/frontend/wsgi.py 1970-01-01 00:00:00 +0000
1091+++ burrowd/frontend/wsgi.py 2011-03-17 23:47:24 +0000
1092@@ -0,0 +1,276 @@
1093+# Copyright (C) 2011 OpenStack LLC.
1094+#
1095+# Licensed under the Apache License, Version 2.0 (the "License");
1096+# you may not use this file except in compliance with the License.
1097+# You may obtain a copy of the License at
1098+#
1099+# http://www.apache.org/licenses/LICENSE-2.0
1100+#
1101+# Unless required by applicable law or agreed to in writing, software
1102+# distributed under the License is distributed on an "AS IS" BASIS,
1103+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1104+# See the License for the specific language governing permissions and
1105+# limitations under the License.
1106+
1107+'''WSGI frontend for the burrow server.'''
1108+
1109+import json
1110+import time
1111+
1112+import eventlet
1113+import eventlet.wsgi
1114+import routes
1115+import routes.middleware
1116+import webob.dec
1117+import webob.exc
1118+
1119+import burrowd.frontend
1120+
1121+# Default configuration values for this module.
1122+DEFAULT_HOST = '0.0.0.0'
1123+DEFAULT_PORT = 8080
1124+DEFAULT_BACKLOG = 64
1125+DEFAULT_SSL = False
1126+DEFAULT_SSL_CERTFILE = 'example.pem'
1127+DEFAULT_SSL_KEYFILE = 'example.key'
1128+DEFAULT_THREAD_POOL_SIZE = 0
1129+DEFAULT_TTL = 600
1130+DEFAULT_HIDE = 0
1131+
1132+
1133+def queue_exists(method):
1134+ '''Decorator to ensure an account and queue exists. If the wait
1135+ option is given, this will block until a message in the queue is
1136+ ready or the timeout expires.'''
1137+ def wrapper(self, req, account, queue, *args, **kwargs):
1138+ wait = 0
1139+ if 'wait' in req.params:
1140+ wait = int(req.params['wait'])
1141+ if wait > 0:
1142+ wait += time.time()
1143+ res = webob.exc.HTTPNotFound()
1144+ while True:
1145+ if self.backend.queue_exists(account, queue):
1146+ res = method(self, req, account, queue, *args, **kwargs)
1147+ if wait == 0 or res.status_int != 404:
1148+ break
1149+ now = time.time()
1150+ if wait - now > 0:
1151+ self.backend.wait(account, queue, wait - now)
1152+ if wait < time.time():
1153+ break
1154+ return res
1155+ return wrapper
1156+
1157+
1158+class Frontend(burrowd.frontend.Frontend):
1159+
1160+ def __init__(self, config, backend):
1161+ super(Frontend, self).__init__(config, backend)
1162+ self.default_ttl = self.config.get('default_ttl', DEFAULT_TTL)
1163+ self.default_hide = self.config.get('default_hide', DEFAULT_HIDE)
1164+ mapper = routes.Mapper()
1165+ mapper.connect('/', action='root')
1166+ mapper.connect('/{account}', action='account')
1167+ mapper.connect('/{account}/{queue}', action='queue')
1168+ mapper.connect('/{account}/{queue}/{message_id}', action='message')
1169+ self._routes = routes.middleware.RoutesMiddleware(self._route, mapper)
1170+
1171+ def run(self, thread_pool):
1172+ '''Create the listening socket and start the thread that runs
1173+ the WSGI server. This extra thread is needed since the WSGI
1174+ server function blocks.'''
1175+ host = self.config.get('host', DEFAULT_HOST)
1176+ port = self.config.getint('port', DEFAULT_PORT)
1177+ backlog = self.config.getint('backlog', DEFAULT_BACKLOG)
1178+ socket = eventlet.listen((host, port), backlog=backlog)
1179+ self.log.info(_('Listening on %s:%d') % (host, port))
1180+ if self.config.getboolean('ssl', DEFAULT_SSL):
1181+ certfile = self.config.get('ssl_certfile', DEFAULT_SSL_CERTFILE)
1182+ keyfile = self.config.get('ssl_keyfile', DEFAULT_SSL_KEYFILE)
1183+ socket = eventlet.green.ssl.wrap_socket(socket, certfile=certfile,
1184+ keyfile=keyfile)
1185+ thread_pool.spawn_n(self._run, socket, thread_pool)
1186+
1187+ def _run(self, socket, thread_pool):
1188+ '''Thread to run the WSGI server.'''
1189+ thread_pool_size = self.config.getint('thread_pool_size',
1190+ DEFAULT_THREAD_POOL_SIZE)
1191+ log_format = '%(client_ip)s "%(request_line)s" %(status_code)s ' \
1192+ '%(body_length)s %(wall_seconds).6f'
1193+ if thread_pool_size == 0:
1194+ eventlet.wsgi.server(socket, self, log=WSGILog(self.log),
1195+ log_format=log_format, custom_pool=thread_pool)
1196+ else:
1197+ eventlet.wsgi.server(socket, self, log=WSGILog(self.log),
1198+ log_format=log_format, max_size=thread_pool_size)
1199+
1200+ def __call__(self, *args, **kwargs):
1201+ return self._routes(*args, **kwargs)
1202+
1203+ @webob.dec.wsgify
1204+ def _route(self, req):
1205+ args = req.environ['wsgiorg.routing_args'][1]
1206+ if not args:
1207+ return webob.exc.HTTPNotFound()
1208+ action = args.pop('action')
1209+ method = getattr(self, '_%s_%s' % (req.method.lower(), action), False)
1210+ if not method:
1211+ return webob.exc.HTTPBadRequest()
1212+ return method(req, **args)
1213+
1214+ @webob.dec.wsgify
1215+ def _delete_root(self, req):
1216+ self.backend.delete_accounts()
1217+ return webob.exc.HTTPNoContent()
1218+
1219+ @webob.dec.wsgify
1220+ def _get_root(self, req):
1221+ accounts = self.backend.get_accounts()
1222+ if len(accounts) == 0:
1223+ return webob.exc.HTTPNotFound()
1224+ return webob.exc.HTTPOk(body=json.dumps(accounts, indent=2))
1225+
1226+ @webob.dec.wsgify
1227+ def _delete_account(self, req, account):
1228+ self.backend.delete_account(account)
1229+ return webob.exc.HTTPNoContent()
1230+
1231+ @webob.dec.wsgify
1232+ def _get_account(self, req, account):
1233+ queues = self.backend.get_queues(account)
1234+ if len(queues) == 0:
1235+ return webob.exc.HTTPNotFound()
1236+ return webob.exc.HTTPOk(body=json.dumps(queues, indent=2))
1237+
1238+ @webob.dec.wsgify
1239+ @queue_exists
1240+ def _delete_queue(self, req, account, queue):
1241+ limit, marker, match_hidden = self._parse_filters(req)
1242+ messages = self.backend.delete_messages(account, queue, limit, marker,
1243+ match_hidden)
1244+ return self._return_messages(req, account, queue, messages, 'none')
1245+
1246+ @webob.dec.wsgify
1247+ @queue_exists
1248+ def _get_queue(self, req, account, queue):
1249+ limit, marker, match_hidden = self._parse_filters(req)
1250+ messages = self.backend.get_messages(account, queue, limit, marker,
1251+ match_hidden)
1252+ return self._return_messages(req, account, queue, messages, 'all')
1253+
1254+ @webob.dec.wsgify
1255+ @queue_exists
1256+ def _post_queue(self, req, account, queue):
1257+ limit, marker, match_hidden = self._parse_filters(req)
1258+ ttl, hide = self._parse_metadata(req)
1259+ messages = self.backend.update_messages(account, queue, limit, marker,
1260+ match_hidden, ttl, hide)
1261+ return self._return_messages(req, account, queue, messages, 'all')
1262+
1263+ @webob.dec.wsgify
1264+ @queue_exists
1265+ def _delete_message(self, req, account, queue, message_id):
1266+ message = self.backend.delete_message(account, queue, message_id)
1267+ if message is None:
1268+ return webob.exc.HTTPNotFound()
1269+ return self._return_message(req, account, queue, message, 'none')
1270+
1271+ @webob.dec.wsgify
1272+ @queue_exists
1273+ def _get_message(self, req, account, queue, message_id):
1274+ message = self.backend.get_message(account, queue, message_id)
1275+ if message is None:
1276+ return webob.exc.HTTPNotFound()
1277+ return self._return_message(req, account, queue, message, 'all')
1278+
1279+ @webob.dec.wsgify
1280+ @queue_exists
1281+ def _post_message(self, req, account, queue, message_id):
1282+ ttl, hide = self._parse_metadata(req)
1283+ message = self.backend.update_message(account, queue, message_id, ttl,
1284+ hide)
1285+ if message is None:
1286+ return webob.exc.HTTPNotFound()
1287+ return self._return_message(req, account, queue, message, 'id')
1288+
1289+ @webob.dec.wsgify
1290+ def _put_message(self, req, account, queue, message_id):
1291+ (ttl, hide) = self._parse_metadata(req, self.default_ttl,
1292+ self.default_hide)
1293+ if self.backend.put_message(account, queue, message_id, ttl, hide, \
1294+ req.body):
1295+ return webob.exc.HTTPCreated()
1296+ return webob.exc.HTTPNoContent()
1297+
1298+ def _filter_message(self, detail, message):
1299+ if detail == 'id':
1300+ return dict(id=message['id'])
1301+ elif detail == 'metadata':
1302+ message = message.copy()
1303+ del message['body']
1304+ return message
1305+ elif detail == 'all':
1306+ return message
1307+ return None
1308+
1309+ def _return_message(self, req, account, queue, message, detail):
1310+ if 'detail' in req.params:
1311+ detail = req.params['detail']
1312+ message = self._filter_message(detail, message)
1313+ if message is not None:
1314+ body = {account: {queue: [message]}}
1315+ return webob.exc.HTTPOk(body=json.dumps(body, indent=2))
1316+ return webob.exc.HTTPNoContent()
1317+
1318+ def _return_messages(self, req, account, queue, messages, detail):
1319+ if len(messages) == 0:
1320+ return webob.exc.HTTPNotFound()
1321+ if 'detail' in req.params:
1322+ detail = req.params['detail']
1323+ filtered_messages = []
1324+ for message in messages:
1325+ message = self._filter_message(detail, message)
1326+ if message is not None:
1327+ filtered_messages.append(message)
1328+ if len(filtered_messages) == 0:
1329+ return webob.exc.HTTPNoContent()
1330+ body = {account: {queue: filtered_messages}}
1331+ return webob.exc.HTTPOk(body=json.dumps(body, indent=2))
1332+
1333+ def _parse_filters(self, req):
1334+ limit = None
1335+ if 'limit' in req.params:
1336+ limit = int(req.params['limit'])
1337+ marker = None
1338+ if 'marker' in req.params:
1339+ marker = req.params['marker']
1340+ match_hidden = False
1341+ if 'hidden' in req.params and req.params['hidden'].lower() == 'true':
1342+ match_hidden = True
1343+ return limit, marker, match_hidden
1344+
1345+ def _parse_metadata(self, req, default_ttl=None, default_hide=None):
1346+ if 'ttl' in req.params:
1347+ ttl = int(req.params['ttl'])
1348+ else:
1349+ ttl = default_ttl
1350+ if ttl is not None and ttl > 0:
1351+ ttl += int(time.time())
1352+ if 'hide' in req.params:
1353+ hide = int(req.params['hide'])
1354+ else:
1355+ hide = default_hide
1356+ if hide is not None and hide > 0:
1357+ hide += int(time.time())
1358+ return ttl, hide
1359+
1360+
1361+class WSGILog(object):
1362+ '''Class for eventlet.wsgi.server to forward logging messages.'''
1363+
1364+ def __init__(self, log):
1365+ self.log = log
1366+
1367+ def write(self, message):
1368+ self.log.debug(message.rstrip())
1369
1370=== added directory 'etc'
1371=== added file 'etc/burrowd.conf'
1372--- etc/burrowd.conf 1970-01-01 00:00:00 +0000
1373+++ etc/burrowd.conf 2011-03-17 23:47:24 +0000
1374@@ -0,0 +1,99 @@
1375+[DEFAULT]
1376+
1377+# Log level to use. All sections below prefixed with 'burrowd' can define
1378+# this to override this default.
1379+log_level = DEBUG
1380+
1381+# Default expiration time in seconds to set for messages.
1382+default_ttl = 600
1383+
1384+# Default hide time in seconds to set for messages.
1385+default_hide = 0
1386+
1387+
1388+[burrowd]
1389+
1390+# Backend to use for storing messages.
1391+backend = burrowd.backend.sqlite
1392+
1393+# Comma separated list of frontends to run.
1394+# frontends = burrowd.frontend.wsgi,burrowd.frontend.wsgi:ssl
1395+frontends = burrowd.frontend.wsgi
1396+
1397+# Size of the thread pool to use for the server.
1398+thread_pool_size = 1000
1399+
1400+
1401+[burrowd.backend.sqlite]
1402+
1403+# Database file to use, passed to sqlite3.connect.
1404+database = :memory:
1405+
1406+
1407+[burrowd.frontend.wsgi]
1408+
1409+# Host to listen on.
1410+host = 0.0.0.0
1411+
1412+# Port to listen on.
1413+port = 8080
1414+
1415+# Size of backlog for listener socket.
1416+backlog = 64
1417+
1418+# Whether to enable SSL.
1419+ssl = False
1420+
1421+# If SSL is enabled, which certfile to use.
1422+ssl_certfile = example.pem
1423+
1424+# If SSL is enabled, which keyfile to use.
1425+ssl_keyfile = example.key
1426+
1427+# Size of thread pool for the WSGI server. If the size is 0, use the main
1428+# burrowd thread pool.
1429+thread_pool_size = 0
1430+
1431+# Default expiration time in seconds to set for messages. This overrides
1432+# the value in the DEFAULT section.
1433+# default_ttl = 600
1434+
1435+# Default hide time in seconds to set for messages. This overrides the
1436+# value in the DEFAULT section.
1437+# default_hide = 0
1438+
1439+
1440+[burrowd.frontend.wsgi:ssl]
1441+
1442+# Port to listen on.
1443+port = 8443
1444+
1445+# Whether to enable SSL.
1446+ssl = True
1447+
1448+
1449+# Logging configuration following the logging.config format.
1450+
1451+[loggers]
1452+keys=root
1453+
1454+[logger_root]
1455+qualname=root
1456+level=WARNING
1457+handlers=console
1458+
1459+[handlers]
1460+keys=console
1461+
1462+[handler_console]
1463+class=StreamHandler
1464+level=DEBUG
1465+formatter=simple
1466+args=(sys.stdout,)
1467+
1468+[formatters]
1469+keys=simple
1470+
1471+[formatter_simple]
1472+format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
1473+datefmt=
1474
1475=== added file 'setup.py'
1476--- setup.py 1970-01-01 00:00:00 +0000
1477+++ setup.py 2011-03-17 23:47:24 +0000
1478@@ -0,0 +1,74 @@
1479+#!/usr/bin/python
1480+# Copyright (C) 2011 OpenStack LLC.
1481+#
1482+# Licensed under the Apache License, Version 2.0 (the "License");
1483+# you may not use this file except in compliance with the License.
1484+# You may obtain a copy of the License at
1485+#
1486+# http://www.apache.org/licenses/LICENSE-2.0
1487+#
1488+# Unless required by applicable law or agreed to in writing, software
1489+# distributed under the License is distributed on an "AS IS" BASIS,
1490+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1491+# See the License for the specific language governing permissions and
1492+# limitations under the License.
1493+
1494+from setuptools import setup, find_packages
1495+from setuptools.command.sdist import sdist
1496+import os
1497+import subprocess
1498+try:
1499+ from babel.messages import frontend
1500+except ImportError:
1501+ frontend = None
1502+
1503+
1504+class local_sdist(sdist):
1505+ """Customized sdist hook - builds the ChangeLog file from VC first"""
1506+
1507+ def run(self):
1508+ if os.path.isdir('.bzr'):
1509+ # We're in a bzr branch
1510+
1511+ log_cmd = subprocess.Popen(["bzr", "log", "--gnu"],
1512+ stdout=subprocess.PIPE)
1513+ changelog = log_cmd.communicate()[0]
1514+ with open("ChangeLog", "w") as changelog_file:
1515+ changelog_file.write(changelog)
1516+ sdist.run(self)
1517+
1518+
1519+name = 'burrow'
1520+
1521+
1522+cmdclass = {'sdist': local_sdist}
1523+
1524+
1525+if frontend:
1526+ cmdclass.update({
1527+ 'compile_catalog': frontend.compile_catalog,
1528+ 'extract_messages': frontend.extract_messages,
1529+ 'init_catalog': frontend.init_catalog,
1530+ 'update_catalog': frontend.update_catalog})
1531+
1532+
1533+setup(
1534+ name=name,
1535+ version='0.1',
1536+ description='Burrow',
1537+ license='Apache License (2.0)',
1538+ author='OpenStack, LLC.',
1539+ author_email='openstack-admins@lists.launchpad.net',
1540+ url='https://launchpad.net/burrow',
1541+ packages=find_packages(exclude=['test', 'bin']),
1542+ test_suite='nose.collector',
1543+ cmdclass=cmdclass,
1544+ classifiers=[
1545+ 'Development Status :: 3 - Alpha',
1546+ 'License :: OSI Approved :: Apache Software License',
1547+ 'Operating System :: POSIX :: Linux',
1548+ 'Programming Language :: Python :: 2.6',
1549+ 'Environment :: No Input/Output (Daemon)'],
1550+ scripts=[
1551+ 'bin/burrow',
1552+ 'bin/burrowd'])
1553
1554=== added directory 'test'
1555=== added file 'test/__init__.py'
1556=== added directory 'test/frontend'
1557=== added file 'test/frontend/__init__.py'
1558=== added file 'test/frontend/test_wsgi.py'
1559--- test/frontend/test_wsgi.py 1970-01-01 00:00:00 +0000
1560+++ test/frontend/test_wsgi.py 2011-03-17 23:47:24 +0000
1561@@ -0,0 +1,313 @@
1562+# Copyright (C) 2011 OpenStack LLC.
1563+#
1564+# Licensed under the Apache License, Version 2.0 (the "License");
1565+# you may not use this file except in compliance with the License.
1566+# You may obtain a copy of the License at
1567+#
1568+# http://www.apache.org/licenses/LICENSE-2.0
1569+#
1570+# Unless required by applicable law or agreed to in writing, software
1571+# distributed under the License is distributed on an "AS IS" BASIS,
1572+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1573+# See the License for the specific language governing permissions and
1574+# limitations under the License.
1575+
1576+import ConfigParser
1577+import json
1578+import time
1579+import unittest
1580+
1581+import eventlet
1582+import webob
1583+
1584+import burrowd.backend.memory
1585+import burrowd.backend.sqlite
1586+import burrowd.frontend.wsgi
1587+
1588+
1589+class TestWSGIMemory(unittest.TestCase):
1590+ '''Unittests for the WSGI frontend to SQLite backend.'''
1591+ backend_class = burrowd.backend.memory.Backend
1592+
1593+ def setUp(self):
1594+ config = (ConfigParser.ConfigParser(), 'test')
1595+ self.backend = self.backend_class(config)
1596+ self.frontend = burrowd.frontend.wsgi.Frontend(config, self.backend)
1597+ self.frontend.default_ttl = 0
1598+ self._get_url('/', status=404)
1599+ self._get_url('/a', status=404)
1600+ self._get_url('/a/q', status=404)
1601+
1602+ def tearDown(self):
1603+ self._get_url('/a/q', status=404)
1604+ self._get_url('/a', status=404)
1605+ self._get_url('/', status=404)
1606+
1607+ def test_account(self):
1608+ self._put_url('/a/q/1')
1609+ accounts = self._get_url('/')
1610+ self.assertEquals(accounts, ['a'])
1611+ self._delete_url('/a')
1612+
1613+ def test_queue(self):
1614+ self._put_url('/a/q/1')
1615+ accounts = self._get_url('/a')
1616+ self.assertEquals(accounts, ['q'])
1617+ self._delete_url('/a/q')
1618+
1619+ def test_message(self):
1620+ self._put_url('/a/q/1', body='b')
1621+ accounts = self._get_url('/a/q')
1622+ self.assertMessages(accounts, 'a', 'q', [self.message('1', body='b')])
1623+ self._delete_url('/a/q/1')
1624+
1625+ def test_message_post(self):
1626+ self._put_url('/a/q/1', body='b')
1627+ for x in range(0, 3):
1628+ accounts = self._post_url('/a/q/1?ttl=%d&hide=%d' % (x, x))
1629+ self.assertEquals(accounts, {'a': {'q': [{'id': '1'}]}})
1630+ accounts = self._get_url('/a/q?hidden=true')
1631+ message = self.message('1', x, x, body='b')
1632+ self.assertMessages(accounts, 'a', 'q', [message])
1633+ self._delete_url('/a/q/1')
1634+
1635+ def test_message_put(self):
1636+ for x in range(0, 3):
1637+ url = '/a/q/1?ttl=%d&hide=%d' % (x, x)
1638+ status = 201 if x == 0 else 204
1639+ self._put_url(url, body=str(x), status=status)
1640+ accounts = self._get_url('/a/q?hidden=true')
1641+ message = self.message('1', x, x, body=str(x))
1642+ self.assertMessages(accounts, 'a', 'q', [message])
1643+ self._delete_url('/a/q/1')
1644+
1645+ def test_message_delete_limit(self):
1646+ [self._put_url('/a/q/%d' % x) for x in range(1, 5)]
1647+ accounts = self._delete_url('/a/q?limit=3&detail=all', status=200)
1648+ messages = []
1649+ messages.append(self.message('1'))
1650+ messages.append(self.message('2'))
1651+ messages.append(self.message('3'))
1652+ self.assertMessages(accounts, 'a', 'q', messages)
1653+ accounts = self._delete_url('/a/q?limit=3&detail=all', status=200)
1654+ message = self.message('4')
1655+ self.assertMessages(accounts, 'a', 'q', [message])
1656+
1657+ def test_message_get_limit(self):
1658+ [self._put_url('/a/q/%d' % x) for x in range(1, 5)]
1659+ for x in range(0, 4):
1660+ accounts = self._get_url('/a/q?limit=3')
1661+ messages = []
1662+ for y in range(x, 4)[:3]:
1663+ messages.append(self.message(str(y + 1)))
1664+ self.assertMessages(accounts, 'a', 'q', messages)
1665+ self._delete_url('/a/q/%d' % (x + 1))
1666+
1667+ def test_message_post_limit(self):
1668+ [self._put_url('/a/q/%d' % x) for x in range(1, 5)]
1669+ for x in range(0, 4):
1670+ accounts = self._post_url('/a/q?limit=3&ttl=%d&detail=all' % x)
1671+ messages = []
1672+ for y in range(x, 4)[:3]:
1673+ messages.append(self.message(str(y + 1), x))
1674+ self.assertMessages(accounts, 'a', 'q', messages)
1675+ self._delete_url('/a/q/%d' % (x + 1))
1676+
1677+ def test_message_delete_marker(self):
1678+ [self._put_url('/a/q/%d' % x) for x in range(1, 5)]
1679+ accounts = self._delete_url('/a/q?marker=2&detail=all', status=200)
1680+ messages = []
1681+ messages.append(self.message('3'))
1682+ messages.append(self.message('4'))
1683+ self.assertMessages(accounts, 'a', 'q', messages)
1684+ accounts = self._delete_url('/a/q?marker=5&detail=all', status=200)
1685+ messages = []
1686+ messages.append(self.message('1'))
1687+ messages.append(self.message('2'))
1688+ self.assertMessages(accounts, 'a', 'q', messages)
1689+
1690+ def test_message_get_marker(self):
1691+ [self._put_url('/a/q/%d' % x) for x in range(1, 5)]
1692+ for x in range(0, 4):
1693+ accounts = self._get_url('/a/q?marker=%d' % x)
1694+ messages = []
1695+ for y in range(x, 4):
1696+ messages.append(self.message(str(y + 1)))
1697+ self.assertMessages(accounts, 'a', 'q', messages)
1698+ self._delete_url('/a/q/%d' % (x + 1))
1699+
1700+ def test_message_post_marker(self):
1701+ [self._put_url('/a/q/%d' % x) for x in range(1, 5)]
1702+ for x in range(0, 4):
1703+ url = '/a/q?marker=%d&ttl=%d&detail=all' % (x, x)
1704+ accounts = self._post_url(url)
1705+ messages = []
1706+ for y in range(x, 4):
1707+ messages.append(self.message(str(y + 1), x))
1708+ self.assertMessages(accounts, 'a', 'q', messages)
1709+ self._delete_url('/a/q/%d' % (x + 1))
1710+
1711+ def test_message_delete_limit_marker(self):
1712+ [self._put_url('/a/q/%d' % x) for x in range(1, 5)]
1713+ url = '/a/q?limit=2&marker=1&detail=all'
1714+ accounts = self._delete_url(url, status=200)
1715+ messages = []
1716+ messages.append(self.message('2'))
1717+ messages.append(self.message('3'))
1718+ self.assertMessages(accounts, 'a', 'q', messages)
1719+ url = '/a/q?limit=2&marker=5&detail=all'
1720+ accounts = self._delete_url(url, status=200)
1721+ messages = []
1722+ messages.append(self.message('1'))
1723+ messages.append(self.message('4'))
1724+ self.assertMessages(accounts, 'a', 'q', messages)
1725+
1726+ def test_message_get_limit_marker(self):
1727+ [self._put_url('/a/q/%d' % x) for x in range(1, 5)]
1728+ for x in range(0, 4):
1729+ accounts = self._get_url('/a/q?limit=2&marker=%d' % x)
1730+ messages = []
1731+ for y in range(x, 4)[:2]:
1732+ messages.append(self.message(str(y + 1)))
1733+ self.assertMessages(accounts, 'a', 'q', messages)
1734+ self._delete_url('/a/q/%d' % (x + 1))
1735+
1736+ def test_message_post_limit_marker(self):
1737+ [self._put_url('/a/q/%d' % x) for x in range(1, 5)]
1738+ for x in range(0, 4):
1739+ url = '/a/q?limit=2&marker=%d&ttl=%d&detail=all' % (x, x)
1740+ accounts = self._post_url(url)
1741+ messages = []
1742+ for y in range(x, 4)[:2]:
1743+ messages.append(self.message(str(y + 1), x))
1744+ self.assertMessages(accounts, 'a', 'q', messages)
1745+ self._delete_url('/a/q/%d' % (x + 1))
1746+
1747+ def test_message_ttl(self):
1748+ self._put_url('/a/q/1?ttl=1')
1749+ accounts = self._get_url('/a/q/1')
1750+ message = self.message('1', 1)
1751+ self.assertMessages(accounts, 'a', 'q', [self.message('1', 1)])
1752+ time.sleep(1)
1753+ self.backend.clean()
1754+ self._get_url('/a/q/1', status=404)
1755+ self._put_url('/a/q/1')
1756+ accounts = self._get_url('/a/q/1')
1757+ self.assertMessages(accounts, 'a', 'q', [self.message('1')])
1758+ self._post_url('/a/q/1?ttl=1')
1759+ accounts = self._get_url('/a/q/1')
1760+ self.assertMessages(accounts, 'a', 'q', [self.message('1', 1)])
1761+ time.sleep(1)
1762+ self.backend.clean()
1763+ self._get_url('/a/q/1', status=404)
1764+
1765+ def test_message_hide(self):
1766+ self._put_url('/a/q/1?hide=1')
1767+ accounts = self._get_url('/a/q/1')
1768+ self.assertMessages(accounts, 'a', 'q', [self.message('1', hide=1)])
1769+ time.sleep(1)
1770+ self.backend.clean()
1771+ accounts = self._get_url('/a/q/1')
1772+ self.assertMessages(accounts, 'a', 'q', [self.message('1')])
1773+ self._post_url('/a/q/1?hide=1')
1774+ accounts = self._get_url('/a/q/1')
1775+ self.assertMessages(accounts, 'a', 'q', [self.message('1', hide=1)])
1776+ time.sleep(1)
1777+ self.backend.clean()
1778+ accounts = self._get_url('/a/q/1')
1779+ self.assertMessages(accounts, 'a', 'q', [self.message('1')])
1780+ self._delete_url('/a/q/1')
1781+
1782+ def _message_wait(self):
1783+ accounts = self._get_url('/a/q?wait=2')
1784+ self.assertMessages(accounts, 'a', 'q', [self.message('1')])
1785+ self.success = True
1786+
1787+ def test_message_put_wait(self):
1788+ self.success = False
1789+ thread = eventlet.spawn(self._message_wait)
1790+ eventlet.spawn_after(0.2, self._put_url, '/a/q/1')
1791+ thread.wait()
1792+ self.assertTrue(self.success)
1793+ self._delete_url('/a/q/1')
1794+
1795+ def test_message_put_wait_overwrite(self):
1796+ self.success = False
1797+ self._put_url('/a/q/1?hide=10')
1798+ thread = eventlet.spawn(self._message_wait)
1799+ eventlet.spawn_after(0.2, self._put_url, '/a/q/1?hide=0', status=204)
1800+ thread.wait()
1801+ self.assertTrue(self.success)
1802+ self._delete_url('/a/q/1')
1803+
1804+ def test_message_put_wait_cleanup(self):
1805+ self.success = False
1806+ self._put_url('/a/q/1?hide=1')
1807+ thread = eventlet.spawn(self._message_wait)
1808+ eventlet.spawn_after(1, self.backend.clean)
1809+ thread.wait()
1810+ self.assertTrue(self.success)
1811+ self._delete_url('/a/q/1')
1812+
1813+ def test_message_post_wait(self):
1814+ self.success = False
1815+ self._put_url('/a/q/1?hide=10')
1816+ thread = eventlet.spawn(self._message_wait)
1817+ eventlet.spawn_after(0.2, self._post_url, '/a/q/1?hide=0')
1818+ thread.wait()
1819+ self.assertTrue(self.success)
1820+ self._delete_url('/a/q/1')
1821+
1822+ def test_message_post_wait_queue(self):
1823+ self.success = False
1824+ self._put_url('/a/q/1?hide=10')
1825+ thread = eventlet.spawn(self._message_wait)
1826+ eventlet.spawn_after(0.2, self._post_url, '/a/q?hide=0&hidden=true')
1827+ thread.wait()
1828+ self.assertTrue(self.success)
1829+ self._delete_url('/a/q/1')
1830+
1831+ def message(self, id, ttl=0, hide=0, body=''):
1832+ return dict(id=id, ttl=ttl, hide=hide, body=body)
1833+
1834+ def assertMessages(self, accounts, account, queue, messages):
1835+ self.assertEquals(len(accounts), 1)
1836+ self.assertEquals(len(accounts['a']), 1)
1837+ self.assertEquals(len(accounts['a']['q']), len(messages))
1838+ for x in range(0, len(messages)):
1839+ self.assertEquals(accounts['a']['q'][x]['id'], messages[x]['id'])
1840+ ttl = messages[x]['ttl']
1841+ if ttl > 0:
1842+ ttl += int(time.time())
1843+ self.assertAlmostEquals(accounts['a']['q'][0]['ttl'], ttl)
1844+ hide = messages[x]['hide']
1845+ if hide > 0:
1846+ hide += int(time.time())
1847+ self.assertAlmostEquals(accounts['a']['q'][0]['hide'], hide)
1848+ body = messages[x]['body']
1849+ self.assertEquals(accounts['a']['q'][x]['body'], body)
1850+
1851+ def _delete_url(self, url, status=204, **kwargs):
1852+ return self._url('DELETE', url, status=status, **kwargs)
1853+
1854+ def _get_url(self, url, **kwargs):
1855+ return self._url('GET', url, **kwargs)
1856+
1857+ def _post_url(self, url, **kwargs):
1858+ return self._url('POST', url, **kwargs)
1859+
1860+ def _put_url(self, url, status=201, **kwargs):
1861+ return self._url('PUT', url, status=status, **kwargs)
1862+
1863+ def _url(self, method, url, body='', status=200):
1864+ req = webob.Request.blank(url, method=method, body=body)
1865+ res = req.get_response(self.frontend)
1866+ self.assertEquals(res.status_int, status)
1867+ if status == 200:
1868+ return json.loads(res.body)
1869+ return None
1870+
1871+
1872+class TestWSGISQLite(TestWSGIMemory):
1873+ '''Unittests for the WSGI frontend to SQLite backend.'''
1874+ backend_class = burrowd.backend.sqlite.Backend

Subscribers

People subscribed via source and target branches