forked from breily/juno
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjuno.py
783 lines (706 loc) · 30.8 KB
/
juno.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
# Built in library imports
import mimetypes
import re
import os
import sys
# Server imports
import urlparse
import cgi
class Juno(object):
def __init__(self, configuration=None):
"""Takes an optional configuration dictionary. """
global _hub
if _hub is not None:
print >>sys.stderr, 'Warning: there is already a Juno object created;'
print >>sys.stderr, ' you might get some weird behavior.'
else: _hub = self
self.routes = []
# Find the directory of the user's app, so we can setup static/template_roots
self.find_user_path()
# Set options and merge in user-set options
self.config = {
# General settings / meta information
'log': True,
'routes': self.routes,
'self': self,
# Types and encodings
'charset': 'utf-8',
'content_type': 'text/html',
# Server options
'mode': 'dev',
'scgi_port': 8000,
'fcgi_port': 8000,
'dev_port': 8000,
# Static file handling
'use_static': True,
'static_url': '/static/*:file/',
'static_root': os.path.join(self.app_path, 'static/'),
'static_handler': static_serve,
# Template options
'use_templates': True,
'template_lib': 'jinja2',
'get_template_handler': _get_template_handler,
'render_template_handler': _render_template_handler,
'auto_reload_templates': True,
'translations': [],
'template_kwargs': {},
'template_root': os.path.join(self.app_path, 'templates/'),
'404_template': '404.html',
'500_template': '500.html',
# Database options
'use_db': True,
'db_type': 'sqlite',
'db_location': ':memory:',
'db_models': {},
# Session options
'use_sessions': False,
'session_lib': 'beaker',
'session_kwargs': {},
# Debugger
'use_debugger': False,
'raise_view_exceptions': False,
# Custom middleware
'middleware': []
}
if configuration is not None: self.config.update(configuration)
# Set up the static file handler
if self.config['use_static']:
self.setup_static()
# Set up templating
if self.config['use_templates']:
self.setup_templates()
# Set up the database
if self.config['use_db']:
self.setup_database()
def find_user_path(self):
# This section may look strange, but it seems like the only solution.
# Basically, we need the directory of the user's app in order to setup
# the static/template_roots. When we run under mod_wsgi, we can't use
# either os.getcwd() or sys.path[0]. This code generates an exception,
# then goes up to the top stack frame (the user's code), and finds
# the location of that.
try:
raise Exception
except:
traceback = sys.exc_info()[2]
if traceback is None:
print >>sys.stderr, 'Warning: Failed to find current working directory.'
print >>sys.stderr, ' Default static_root and template_root'
print >>sys.stderr, ' values may not work correctly.'
filename = './'
else:
frame = traceback.tb_frame
while traceback is not None:
if frame.f_back is None:
break
frame = frame.f_back
filename = frame.f_code.co_filename
self.app_path = os.path.abspath(os.path.dirname(filename))
def setup_static(self):
self.route(self.config['static_url'], self.config['static_handler'], '*')
def setup_templates(self):
if self.config['template_lib'] == 'jinja2':
import jinja2
# If the user specified translation objects, load i18n extension
if len(self.config['translations']) != 0:
extensions = ['jinja2.ext.i18n']
else:
extensions = ()
self.config['template_env'] = jinja2.Environment(
loader = jinja2.FileSystemLoader(
searchpath = self.config['template_root'],
encoding = self.config['charset'],
),
auto_reload = self.config['auto_reload_templates'],
extensions = extensions,
**self.config['template_kwargs']
)
for translation in self.config['translations']:
self.config['template_env'].install_gettext_translations(translation)
if self.config['template_lib'] == 'mako':
import mako.lookup
self.config['template_env'] = mako.lookup.TemplateLookup(
directories = [self.config['template_root']],
input_encoding = self.config['charset'],
output_encoding = self.config['charset'],
filesystem_checks = self.config['auto_reload_templates'],
**self.config['template_kwargs']
)
def setup_database(self):
# DB library imports
from sqlalchemy import (create_engine, Table, MetaData, Column, Integer,
String, Unicode, Text, UnicodeText, Date, Numeric,
Time, Float, DateTime, Interval, Binary, Boolean,
PickleType)
from sqlalchemy.orm import sessionmaker, mapper
# Create global name mappings for model()
global column_mapping
column_mapping = {'string': String, 'str': String,
'integer': Integer, 'int': Integer,
'unicode': Unicode, 'text': Text,
'unicodetext': UnicodeText, 'date': Date,
'numeric': Numeric, 'time': Time,
'float': Float, 'datetime': DateTime,
'interval': Interval, 'binary': Binary,
'boolean': Boolean, 'bool': Boolean,
'pickletype': PickleType,
}
# Add a few SQLAlchemy types to globals() so we can use them in models
globals().update({'Column': Column, 'Table': Table, 'Integer': Integer,
'MetaData': MetaData, 'mapper': mapper})
# Ensures correct slash number for sqlite
if self.config['db_type'] == 'sqlite':
self.config['db_location'] = '/' + self.config['db_location']
eng_name = self.config['db_type'] + '://' + self.config['db_location']
self.config['db_engine'] = create_engine(eng_name)
self.config['db_session'] = sessionmaker(bind=self.config['db_engine'])()
def run(self, mode=None):
"""Runs the Juno hub, in the set mode (default now is dev). """
# If no mode is specified, use the one from the config
if mode is None: mode = config('mode')
# Otherwise store the specified mode
else: config('mode', mode)
if mode == 'dev': run_dev('', config('dev_port'), self.request)
elif mode == 'scgi': run_scgi('', config('scgi_port'), self.request)
elif mode == 'fcgi': run_fcgi('', config('fcgi_port'), self.request)
elif mode == 'wsgi': return run_wsgi(self.request)
elif mode == 'appengine': run_appengine(self.request)
else:
print >>sys.stderr, 'Error: unrecognized mode'
print >>sys.stderr, ' exiting juno...'
def request(self, request, method='*', **kwargs):
"""Called when a request is received. Routes a url to its view.
Returns a 3-tuple (status_string, headers, body) from
JunoResponse.render()."""
if config('log'): print '%s request for %s...' %(method, request)
req_obj = JunoRequest(kwargs)
# Set the global response object in case the view wants to use it
global _response
_response = JunoResponse()
# Add a slash if there isn't one - avoids frustrating matching bugs
if request[-1] != '/': request += '/'
for route in self.routes:
if not route.match(request, method): continue
if config('log'): print '%s matches, calling %s()...\n' %(
route.old_url, route.func.__name__)
# Get the return from the view
if config('raise_view_exceptions') or config('use_debugger'):
response = route.dispatch(req_obj)
else:
try:
response = route.dispatch(req_obj)
except:
return servererror(error=cgi.escape(str(sys.exc_info()))).render()
# If nothing returned, use the global object
if response is None: response = _response
# If we don't have a string, render the Response to one
if isinstance(response, JunoResponse):
return response.render()
return JunoResponse(body=response).render()
# No matches - 404
return notfound(error='No matching routes registered').render()
def route(self, url, func, method):
"""Attaches a view to a url or list of urls, for a given function. """
# An implicit route - the url is just the function name
if url is None: url = '/' + func.__name__ + '/'
# If we just have one url, add it
if type(url) == str: self.routes.append(JunoRoute(url, func, method))
# Otherwise add each url in the list
else:
for u in url: self.routes.append(JunoRoute(u, func, method))
def __getattr__(self, attr):
if attr in self.config.keys():
return self.config[attr]
return None
def __repr__(self): return '<Juno>'
class JunoRoute(object):
"""Uses a simplified regex to grab url parts:
i.e., '/hello/*:name/' compiles to '^/hello/(?P<name>\w+)/' """
def __init__(self, url, func, method):
# Make sure the url begins and ends in a '/'
if url[0] != '/': url = '/' + url
if url[-1] != '/': url += '/'
# Store the old one before we modify it (we use it for __repr__)
self.old_url = url
# RE to match the splat format
splat_re = re.compile('^\*?:(?P<var>\w+)$')
# Start building our modified url
buffer = '^'
for part in url.split('/'):
# Beginning and end entries are empty, so skip them
if not part: continue
match_obj = splat_re.match(part)
# If it doesn't match, just add it without modification
if match_obj is None: buffer += '/' + part
# Otherwise replace it with python's regex format
else: buffer += '/(?P<' + match_obj.group('var') + '>.*)'
# If we don't end with a wildcard, add a end of line modifier
if buffer[-1] != ')': buffer += '/$'
else: buffer += '/'
self.url = re.compile(buffer)
self.func = func
self.method = method.upper()
self.params = {}
def match(self, request, method):
"""Matches a request uri to this url object. """
match_obj = self.url.match(request)
if match_obj is None: return False
# Make sure the request method matches
if self.method != '*' and self.method != method: return False
# Store the parts that matched
self.params.update(match_obj.groupdict())
return True
def dispatch(self, req):
"""Calls the route's view with any named parameters."""
return self.func(req, **self.params)
def __repr__(self):
return '<JunoRoute: %s %s - %s()>' %(self.method, self.old_url,
self.func.__name__)
class JunoRequest(object):
"""Offers following members:
raw => the header dict used to construct the JunoRequest
location => uri being requested, without query string ('/' from '/?a=6')
full_location => uri with query string ('/?a=6' from '/?a=6')
user_agent => the user agent string of requester
"""
def __init__(self, request):
# Make sure we have a request uri, and it ends in '/'
if request['PATH_INFO'][-1] != '/': request['PATH_INFO'] += '/'
# Set some instance variables
self.raw = request
self.raw['input'] = {}
self.location = request['PATH_INFO']
# If we get a REQUEST_URI, store it. Otherwise copy PATH_INFO
if 'REQUEST_URI' in request:
self.full_location = request['REQUEST_URI']
else: self.full_location = self.location
# Find the right user agent header
if 'HTTP_USER_AGENT' in request:
self.user_agent = request['HTTP_USER_AGENT']
elif 'User-Agent' in request:
self.user_agent = request['User-Agent']
else: self.user_agent = ''
self.combine_request_dicts()
# Check for sessions
if config('use_sessions') and config('session_lib') == 'beaker':
self.session = request['beaker.session']
def combine_request_dicts(self):
input_dict = self.raw['QUERY_DICT'].copy()
for k, v in self.raw['POST_DICT'].items():
# Combine repeated keys
if k in input_dict.keys(): input_dict[k].extend(v)
# Otherwise just add this key
else: input_dict[k] = v
# Reduce the dict - change one item lists ([a] to a)
for k, v in input_dict.items():
if len(v) == 1: input_dict[k] = v[0]
self.raw['input'] = input_dict
def __getattr__(self, attr):
# Try returning values from self.raw
if attr in self.keys(): return self.raw[attr]
if attr == 'session' and config('log'):
print >>sys.stderr, "Error: To use sessions, enable 'use_sessions'"
print >>sys.stderr, " when calling juno.init()"
print >>sys.stderr, ""
return None
def input(self, arg=None):
# No args: return the whole dictionary
if arg is None: return self.raw['input']
# Otherwise try to return the value for that key
if self.raw['input'].has_key(arg):
return self.raw['input'][arg]
return None
# Make JunoRequest act as a dictionary for self.raw
def __getitem__(self, key): return self.raw[key]
def __setitem__(self, key, val): self.raw[key] = val
def keys(self): return self.raw.keys()
def items(self): return self.raw.items()
def values(self): return self.raw.values()
def __len__(self): return len(self.raw)
def __contains__(self, key): return key in self.raw
def __repr__(self):
return '<JunoRequest: %s>' %self.location
class JunoResponse(object):
status_codes = {
200: 'OK',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
400: 'Bad Request',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
410: 'Gone',
500: 'Internal Server Error',
}
def __init__(self, configuration=None, **kwargs):
# Set options and merge in user-set options
self.config = {
'body': '',
'status': 200,
'headers': { 'Content-Type': config('content_type'), },
}
if configuration is None: configuration = {}
self.config.update(configuration)
self.config.update(kwargs)
self.config['headers']['Content-Length'] = len(self.config['body'])
# Add text and adjust content-length
def append(self, text):
self.config['body'] += str(text)
self.config['headers']['Content-Length'] = len(self.config['body'])
return self
# Implement +=
def __iadd__(self, text):
return self.append(text)
def render(self):
"""Returns a 3-tuple (status_string, headers, body)."""
status_string = '%s %s' %(self.config['status'],
self.status_codes[self.config['status']])
headers = [(k, str(v)) for k, v in self.config['headers'].items()]
body = '%s' %self.config['body']
return (status_string, headers, body)
# Set a header value
def header(self, header, value):
self.config['headers'][header] = value
return self
# Modify the headers dictionary when the response is treated like a dict
def __setitem__(self, header, value): self.header(header, value)
def __getitem__(self, header): return self.config['headers'][header]
def __getattr__(self, attr):
return self.config[attr]
def __repr__(self):
return '<JunoResponse: %s %s>' %(self.status, self.status_codes[self.status])
#
# Functions to deal with the global Juno object (_hub)
#
_hub = None
def init(configuration=None):
"""Set up Juno with an optional configuration."""
# Only set it up if we haven't already. (Avoids multiple Juno objects)
global _hub # Use global keyword here to avoid SyntaxWarning
if _hub is None:
_hub = Juno(configuration)
return _hub
def config(key, value=None):
"""Get or set configuration options."""
if _hub is None: init()
if value is None:
# Either pass a configuration dictionary
if type(key) == dict: _hub.config.update(key)
# Or retrieve a value
else:
if key in _hub.config.keys():
return _hub.config[key]
return None
# Or set a specific value
else: _hub.config[key] = value
def run(mode=None):
"""Start Juno, with an optional mode argument."""
if _hub is None: init()
if len(sys.argv) > 1:
if '-mode=' in sys.argv[1]: mode = sys.argv[1].split('=')[1]
elif '-mode' == sys.argv[1]: mode = sys.argv[2]
return _hub.run(mode)
#
# Decorators to add routes based on request methods
#
def route(url=None, method='*'):
if _hub is None: init()
def wrap(f): _hub.route(url, f, method)
return wrap
def post(url=None): return route(url, 'post')
def get(url=None): return route(url, 'get')
def head(url=None): return route(url, 'head')
def put(url=None): return route(url, 'put')
def delete(url=None): return route(url, 'delete')
#
# Functions to deal with the global response object (_response)
#
_response = None
def append(body):
"""Add text to response body. """
global _response
return _response.append(body)
def header(key, value):
"""Set a response header. """
global _response
return _response.header(key, value)
def content_type(type):
"""Set the content type header. """
header('Content-Type', type)
def status(code):
_response.config['status'] = code
#
# Convenience functions for 404s and redirects
#
def redirect(url, code=302):
status(code)
# clear the response headers and add the location header
_response.config['headers'] = { 'Location': url }
return _response
def assign(from_, to):
if type(from_) not in (list, tuple): from_ = [from_]
for url in from_:
@route(url)
def temp(web): redirect(to)
def notfound(error='Unspecified error', file=None):
"""Sets the response to a 404, sets the body to 404_template."""
if config('log'): print >>sys.stderr, 'Not Found: %s' % error
status(404)
if file is None: file = config('404_template')
return template(file, error=error)
def servererror(error='Unspecified error', file=None):
"""Sets the response to a 500, sets the body to 500_template."""
if config('log'): print >>sys.stderr, 'Error: (%s, %s, %s)' % sys.exc_info()
status(500)
if file is None: file = config('500_template')
# Resets the response, in case the error occurred as we added data to it
_response.config['body'] = ''
return template(file, error=error)
#
# Serve static files.
#
def static_serve(web, file):
"""The default static file serve function. Maps arguments to dir structure."""
file = os.path.join(config('static_root'), file)
realfile = os.path.realpath(file)
if not realfile.startswith(os.path.realpath(config('static_root'))):
notfound("that file could not be found/served")
elif yield_file(file) != 7:
notfound("that file could not be found/served")
def yield_file(filename, type=None):
"""Append the content of a file to the response. Guesses file type if not
included. Returns 1 if requested file can't be accessed (often means doesn't
exist). Returns 2 if requested file is a directory. Returns 7 on success. """
if not os.access(filename, os.F_OK): return 1
if os.path.isdir(filename): return 2
if type is None:
guess = mimetypes.guess_type(filename)[0]
if guess is None: content_type('text/plain')
else: content_type(guess)
else: content_type(type)
append(open(filename, 'r').read())
return 7
#
# Templating
#
def template(template_path, template_dict=None, **kwargs):
"""Append a rendered template to response. If template_dict is provided,
it is passed to the render function. If not, kwargs is."""
# Retreive a template object.
t = get_template(template_path)
# Render it without arguments.
if not kwargs and not template_dict:
return append(render_template(t))
# Render the template with a provided template dictionary
if template_dict:
return append(render_template(t, **template_dict))
# Render the template with **kwargs
return append(render_template(t, **kwargs))
def get_template(template_path):
"""Returns a template object by calling the default value of
'get_template_handler'. Allows getting a template to be the same
regardless of template library."""
return config('get_template_handler')(template_path)
# The default value of config('get_template_handler')
def _get_template_handler(template_path):
"""Return a template object. This is defined for the Jinja2 and
Mako libraries, otherwise you have to override it. Takes one
parameter: a string containing the desired template path. Needs
to return an object that will be passed to your rendering function."""
return config('template_env').get_template(template_path)
def render_template(template_obj, **kwargs):
"""Renders a template object by using the default value of
'render_template_handler'. Allows rendering a template to be consistent
regardless of template library."""
return config('render_template_handler')(template_obj, **kwargs)
# The default value of config('render_template_handler')
def _render_template_handler(template_obj, **kwargs):
"""Renders template object with an optional dictionary of values.
Defined for Jinja2 and Mako - override it if you use another
library. Takes a template object as the first parameter, with an
optional **kwargs parameter. Needs to return a string."""
if config('template_lib') == 'mako': return template_obj.render(**kwargs)
if config('template_lib') == 'jinja2':
# Jinja needs its output encoded here
return template_obj.render(**kwargs).encode(config('charset'))
def autotemplate(urls, template_path):
"""Automatically renders a template for a given path. Currently can't
use any arguments in the url."""
if type(urls) not in (list, tuple): urls = urls[urls]
for url in urls:
@route(url)
def temp(web): template(template_path)
####
# Data modeling functions
####
class JunoClassConstructor(type):
def __new__(cls, name, bases, dct):
return type.__new__(cls, name, bases, dct)
def __init__(cls, name, bases, dct):
super(JunoClassConstructor, cls).__init__(name, bases, dct)
# Map SQLAlchemy's types to string versions of them for convenience
column_mapping = {} # Constructed in Juno.setup_database
session = lambda: config('db_session')
def model(model_name, **kwargs):
if not _hub: init()
# Functions for the class
def __init__(self, **kwargs):
for k, v in kwargs.items():
self.__dict__[k] = v
def add(self):
session().add(self)
return self
def save(self):
s = session()
if self not in s: s.add(self)
s.commit()
return self
def __repr__(self):
return '<%s: id = %s>' %(self.__name__, self.id)
def __str__(self):
return '<%s: id = %s>' %(self.__name__, self.id)
# Some static functions for the class
def find_func(cls):
return session().query(cls)
# Build the class's __dict__
cls_dict = {'__init__': __init__,
'add': add,
'save': save,
'__name__': model_name,
'__repr__': __repr__,
'__str__': __str__,
'find': None, # Defined later
}
# Parse kwargs to get column definitions
cols = [ Column('id', Integer, primary_key=True), ]
for k, v in kwargs.items():
if callable(v):
cls_dict[k] = v
elif isinstance(v, Column):
if not v.name: v.name = k
cols.append(v)
elif type(v) == str:
v = v.lower()
if v in column_mapping: v = column_mapping[v]
else: raise NameError("'%s' is not an allowed database column" %v)
cols.append(Column(k, v))
# Create the class
tmp = JunoClassConstructor(model_name, (object,), cls_dict)
# Add the functions that need an instance of the class
tmp.find = staticmethod(lambda: find_func(tmp))
# Create the table
metadata = MetaData()
tmp_table = Table(model_name + 's', metadata, *cols)
metadata.create_all(config('db_engine'))
# Map the table to the created class
mapper(tmp, tmp_table)
# Track the class
config('db_models')[model_name] = tmp
return tmp
def find(model_cls):
if isinstance(mode_cls, str):
try: model_cls = config('db_models')[model_cls]
except: raise NameError("No such model exists ('%s')" %model_cls)
return session().query(model_cls)
####
# Juno's Servers - Development (using WSGI), and SCGI (using Flup)
####
def get_application(process_func):
def application(environ, start_response):
# This may be temporary - I was getting weird errors where
# the `environ` was None. Seems to have stopped, but I don't
# know why. This message was to clarify what happened.
if environ is None:
print >>sys.stderr, 'Error: environ is None for some reason.'
print >>sys.stderr, 'Error: environ=%s' %environ
sys.exit()
# Ensure some variable exist (WSGI doesn't guarantee them)
if 'PATH_INFO' not in environ.keys() or not environ['PATH_INFO']:
environ['PATH_INFO'] = '/'
if 'QUERY_STRING' not in environ.keys():
environ['QUERY_STRING'] = ''
if 'CONTENT_LENGTH' not in environ.keys() or not environ['CONTENT_LENGTH']:
environ['CONTENT_LENGTH'] = '0'
# Standardize some header names
environ['DOCUMENT_URI'] = environ['PATH_INFO']
if environ['QUERY_STRING']:
environ['REQUEST_URI'] = environ['PATH_INFO']+'?'+environ['QUERY_STRING']
else:
environ['REQUEST_URI'] = environ['DOCUMENT_URI']
# Parse query string arguments
environ['QUERY_DICT'] = cgi.parse_qs(environ['QUERY_STRING'],
keep_blank_values=1)
if environ['REQUEST_METHOD'] in ('POST', 'PUT'):
fs = cgi.FieldStorage(fp=environ['wsgi.input'],
environ=environ,
keep_blank_values=True)
post_dict = {}
if fs.list:
for field in fs.list:
if field.filename:
value = field
else:
value = field.value
# Each element of post_dict will be a list, even if it contains only
# one item. This is in line with QUERY_DICT which also works like this.
if not field.name in post_dict:
post_dict[field.name] = [value]
else:
post_dict[field.name].append(value)
environ['POST_DICT'] = post_dict
else: environ['POST_DICT'] = {}
# Done parsing inputs, now ready to send to Juno
status_str, headers, body = process_func(environ['PATH_INFO'],
environ['REQUEST_METHOD'],
**environ)
start_response(status_str, headers)
return [body]
middleware_list = []
if config('use_debugger'):
middleware_list.append(('werkzeug.DebuggedApplication', {'evalex': True}))
if config('use_sessions') and config('session_lib') == 'beaker':
middleware_list.append(('beaker.middleware.SessionMiddleware', config('session_kwargs')))
middleware_list.extend(config('middleware'))
application = _load_middleware(application, middleware_list)
return application
def _load_middleware(application, middleware_list):
for middleware, args in middleware_list:
parts = middleware.split('.')
module = '.'.join(parts[:-1])
name = parts[-1]
try:
obj = getattr(__import__(module, None, None, [name]), name)
application = obj(application, **args)
except (ImportError, AttributeError):
print 'Warning: failed to load middleware %s' % name
return application
def run_dev(addr, port, process_func):
from wsgiref.simple_server import make_server
app = get_application(process_func)
print ''
print 'running Juno development server, <C-c> to exit...'
print 'connect to 127.0.0.1:%s to use your app...' %port
print ''
srv = make_server(addr, port, app)
try:
srv.serve_forever()
except:
print 'interrupted; exiting juno...'
srv.socket.close()
def run_scgi(addr, port, process_func):
from flup.server.scgi_fork import WSGIServer as SCGI
app = get_application(process_func)
SCGI(application=app, bindAddress=(addr, port)).run()
def run_fcgi(addr, port, process_func):
from flup.server.fcgi import WSGIServer as FCGI
app = get_application(process_func)
FCGI(app, bindAddress=(addr, port)).run()
def run_wsgi(process_func):
sys.stdout = sys.stderr
return get_application(process_func)
def run_appengine(process_func):
sys.stdout = sys.stderr
from google.appengine.ext.webapp.util import run_wsgi_app
run_wsgi_app(get_application(process_func))