├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README ├── README.rst ├── airy ├── __init__.py ├── bin │ └── airy-admin.py ├── contrib │ ├── __init__.py │ └── api │ │ ├── __init__.py │ │ └── handlers.py ├── core │ ├── __init__.py │ ├── admin.py │ ├── conf.py │ ├── context_processors.py │ ├── db.py │ ├── decorators.py │ ├── exceptions.py │ ├── files │ │ ├── __init__.py │ │ ├── base.py │ │ ├── images.py │ │ ├── locks.py │ │ ├── move.py │ │ ├── storage.py │ │ ├── temp.py │ │ ├── uploadedfile.py │ │ ├── uploadhandler.py │ │ └── utils.py │ ├── mail │ │ ├── __init__.py │ │ ├── backends │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── console.py │ │ │ ├── dummy.py │ │ │ ├── filebased.py │ │ │ ├── locmem.py │ │ │ └── smtp.py │ │ ├── message.py │ │ └── utils.py │ ├── manager.py │ ├── markup.py │ ├── monitor.py │ ├── reportbug.py │ ├── sanitizer.py │ ├── serializers │ │ ├── __init__.py │ │ ├── base.py │ │ └── json.py │ ├── validators.py │ ├── ve.py │ └── web.py ├── forms │ ├── __init__.py │ ├── extras │ │ ├── __init__.py │ │ └── widgets.py │ ├── fields.py │ ├── forms.py │ ├── formsets.py │ ├── util.py │ └── widgets.py ├── skeleton │ ├── app │ │ ├── __init__.py │ │ ├── handlers.py │ │ ├── models.py │ │ └── urls.py │ └── project │ │ ├── __init__.py │ │ ├── flashpolicy.xml │ │ ├── manage.py │ │ ├── requirements.pip │ │ ├── settings.py │ │ ├── static │ │ ├── images │ │ │ ├── misc │ │ │ │ ├── button-gloss.png │ │ │ │ ├── button-overlay.png │ │ │ │ ├── custom-form-sprites.png │ │ │ │ ├── input-bg.png │ │ │ │ ├── modal-gloss.png │ │ │ │ └── table-sorter.png │ │ │ ├── orbit │ │ │ │ ├── bullets.jpg │ │ │ │ ├── left-arrow.png │ │ │ │ ├── loading.gif │ │ │ │ ├── mask-black.png │ │ │ │ ├── pause-black.png │ │ │ │ ├── right-arrow.png │ │ │ │ ├── rotator-black.png │ │ │ │ └── timer-black.png │ │ │ ├── profile-default-icon.png │ │ │ └── profile-default.png │ │ ├── javascripts │ │ │ ├── jquery.editableText.js │ │ │ └── project.js │ │ ├── media │ │ │ └── README │ │ └── stylesheets │ │ │ ├── bootstrap.css │ │ │ ├── ie.css │ │ │ └── style.css │ │ ├── templates │ │ ├── 404.html │ │ ├── base.html │ │ ├── index.html │ │ ├── page.html │ │ └── users │ │ │ ├── login.html │ │ │ ├── page.html │ │ │ ├── password_recovery │ │ │ ├── new_password.html │ │ │ ├── password_reset_sent.html │ │ │ └── recovery.html │ │ │ ├── profile │ │ │ ├── bio.html │ │ │ ├── contact.html │ │ │ ├── first_name.html │ │ │ ├── foreign_profile.html │ │ │ ├── headline.html │ │ │ ├── last_name.html │ │ │ ├── main.html │ │ │ ├── other_sites.html │ │ │ ├── picture.html │ │ │ └── picture_upload.html │ │ │ ├── register.html │ │ │ ├── user_in_menu.html │ │ │ └── user_out_menu.html │ │ └── users │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── context_processors.py │ │ ├── decorators.py │ │ ├── forms.py │ │ ├── handlers.py │ │ ├── models.py │ │ └── urls.py ├── static │ ├── WebSocketMain.swf │ ├── airy.js │ ├── lib │ │ ├── jquery.cookie.js │ │ ├── jquery.history.js │ │ ├── jquery.min.js │ │ └── jquery.serializeForm.js │ └── socket.io.js └── utils │ ├── __init__.py │ ├── _os.py │ ├── _threading_local.py │ ├── autoreload.py │ ├── cache.py │ ├── checksums.py │ ├── copycompat.py │ ├── crypto.py │ ├── daemonize.py │ ├── datastructures.py │ ├── dateformat.py │ ├── dates.py │ ├── datetime_safe.py │ ├── decorators.py │ ├── dictconfig.py │ ├── encoding.py │ ├── feedgenerator.py │ ├── formats.py │ ├── functional.py │ ├── hashcompat.py │ ├── html.py │ ├── http.py │ ├── importlib.py │ ├── itercompat.py │ ├── log.py │ ├── module_loading.py │ ├── numberformat.py │ ├── regex_helper.py │ ├── safestring.py │ ├── simplejson │ ├── LICENSE.txt │ ├── __init__.py │ ├── decoder.py │ ├── encoder.py │ ├── scanner.py │ └── tool.py │ ├── stopwords.py │ ├── synch.py │ ├── termcolors.py │ ├── text.py │ ├── timesince.py │ ├── translation │ ├── __init__.py │ ├── trans_null.py │ └── trans_real.py │ ├── tree.py │ ├── tzinfo.py │ ├── unittest │ ├── __init__.py │ ├── __main__.py │ ├── case.py │ ├── collector.py │ ├── compatibility.py │ ├── loader.py │ ├── main.py │ ├── result.py │ ├── runner.py │ ├── signals.py │ ├── suite.py │ └── util.py │ ├── version.py │ └── xmlutils.py ├── doc ├── Makefile ├── changelog.rst ├── conf.py ├── index.rst ├── intro.rst ├── make.bat ├── reference.rst ├── reference │ ├── 00_project.rst │ ├── 01_handlers.rst │ ├── 02_database.rst │ ├── 03_settings.rst │ ├── 04_templates.rst │ └── 05_console.rst └── tutorial.rst └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | */.ve/* 3 | .idea/ 4 | *.swp 5 | .DS_Store 6 | .tmp* 7 | _build 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, LetoLab Ltd 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include airy/skeleton *.py *.html *.css *.js *.png *.jpg *.xml *.pip README 2 | recursive-include airy/static * 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ============================== 2 | Airy Web Application Framework 3 | ============================== 4 | 5 | Airy is a new Web application development framework. 6 | 7 | Contrast to most currently available frameworks, Airy 8 | doesn't use the standard notion of HTTP requests and pages. 9 | 10 | Instead, it makes use of WebSockets (via Socket.io) and 11 | provides a set of tools to let you focus on the interface, 12 | not content delivery. 13 | 14 | Currently Airy supports MongoDB only. We will support other 15 | NoSQL databases, but we have no plans for supporting SQL. 16 | 17 | 18 | Requirements 19 | ============ 20 | 21 | Airy will install most required modules itself when you create a new 22 | project, so all you need is: 23 | 24 | * Python 2.6+ 25 | * MongoDB 26 | 27 | 28 | Installation 29 | ============ 30 | 31 | pip install airy 32 | 33 | This will install Airy itself and the ``airy-admin.py`` script. 34 | 35 | 36 | Usage 37 | ===== 38 | 39 | Once you have it installed, you should be able to use ``airy-admin.py`` 40 | 41 | To create a new project, open a terminal and do:: 42 | 43 | airy-admin.py startproject project_name 44 | cd project_name/ 45 | python manage.py update_ve 46 | python manage.py runserver 47 | 48 | You should have it running locally on port 8000. Open your browser 49 | and navigate to http://localhost:8000 50 | 51 | Note: if it complains about a "Connection Error" it means you have 52 | no MongoDB running, or it's dead. 53 | 54 | 55 | About 56 | ===== 57 | 58 | Airy is created by Leto, a startup agency based in London, UK. 59 | Check out Leto website for help and support: http://letolab.com 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /airy/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'LetoLab Ltd' 2 | 3 | VERSION = (0, 3, 1) 4 | 5 | def get_version(): 6 | version = '%s.%s' % (VERSION[0], VERSION[1]) 7 | if VERSION[2]: 8 | version = '%s.%s' % (version, VERSION[2]) 9 | return version 10 | 11 | __version__ = get_version() 12 | -------------------------------------------------------------------------------- /airy/bin/airy-admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from airy.core import admin 4 | 5 | if __name__ == "__main__": 6 | admin.execute() 7 | -------------------------------------------------------------------------------- /airy/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/letolab/airy/4dc53bec4aa0cd9b983a0a626fecdf49d14bdf94/airy/contrib/__init__.py -------------------------------------------------------------------------------- /airy/contrib/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/letolab/airy/4dc53bec4aa0cd9b983a0a626fecdf49d14bdf94/airy/contrib/api/__init__.py -------------------------------------------------------------------------------- /airy/contrib/api/handlers.py: -------------------------------------------------------------------------------- 1 | from airy.core.web import * 2 | from airy.core.db import * 3 | from airy.core.serializers.json import JSONSerializer 4 | 5 | def expose_method(f): 6 | def wrapped(self, *args, **kwargs): 7 | if f.__name__ in self.methods: 8 | return f(self, *args, **kwargs) 9 | else: 10 | self.write("Method Not Available") 11 | self.finish() 12 | return wrapped 13 | 14 | class APIHandler(AiryRequestHandler): 15 | model = Document 16 | serializer = JSONSerializer 17 | fields = set() 18 | exclude = set() 19 | levels = 1 20 | methods = ('get', 'post', 'put', 'delete') 21 | 22 | def __init__(self, *args, **kwargs): 23 | super(APIHandler, self).__init__(*args, **kwargs) 24 | 25 | if not self.fields: 26 | self.fields = set(self.model._fields.keys()) 27 | 28 | def _generate_model(self, **kwargs): 29 | model_fields = {} 30 | for key, value in kwargs.items(): 31 | field = self.model._fields.get(key, None) 32 | if field: 33 | if isinstance(field, ReferenceField): 34 | value = field.document_type.objects.get(pk=value) 35 | model_fields[key] = value 36 | return self.model(**model_fields) 37 | 38 | def check_xsrf_cookie(self): 39 | pass 40 | 41 | def deserialize_query(self, query_dict): 42 | for field_name in query_dict: 43 | field = self.model._fields.get(field_name) 44 | if isinstance(field, BooleanField): 45 | query_dict[field_name] = bool(query_dict[field_name]) 46 | if isinstance(field, IntField): 47 | query_dict[field_name] = int(query_dict[field_name]) 48 | 49 | return query_dict 50 | 51 | def get_filter_query(self): 52 | arguments = self.get_flat_arguments() 53 | use_fields = set(self.fields) & set(arguments.keys()) 54 | use_fields = set(use_fields) - set(self.exclude) 55 | query_dict = dict((field, arguments[field]) for field in use_fields) 56 | query_dict = self.deserialize_query(query_dict) 57 | return Q(**query_dict) 58 | 59 | def get_queryset(self, id=None): 60 | try: 61 | if id: 62 | queryset = self.model.objects.get(id=id) 63 | else: 64 | queryset = self.model.objects.filter(self.get_filter_query()) 65 | except Exception, e: 66 | if settings.debug: 67 | raise 68 | logging.warn("API Error: %s" % e) 69 | queryset = None 70 | return queryset 71 | 72 | def serialize(self, queryset): 73 | try: 74 | return self.serializer(levels=self.levels, fields=self.fields, exclude=self.exclude).serialize(queryset) 75 | except ValidationError, e: 76 | logging.warn("API Error: %s" % e) 77 | if settings.debug: 78 | return "API Error: %s" % e 79 | else: 80 | return '' 81 | 82 | @report_on_fail 83 | @expose_method 84 | def get(self, id=None): 85 | queryset = self.get_queryset(id) 86 | self.set_header("Content-Type", "application/json") 87 | self.write(self.serialize(queryset)) 88 | self.finish() 89 | 90 | @report_on_fail 91 | @expose_method 92 | def put(self, id=None): 93 | queryset = self.get_queryset(id) 94 | if queryset: 95 | queryset.update(**dict([("set__%s" % key, value) for key, value in self.get_flat_arguments().items()])) 96 | self.set_header("Content-Type", "application/json") 97 | self.write(self.serialize(queryset)) 98 | self.finish() 99 | 100 | @report_on_fail 101 | @expose_method 102 | def post(self, id=None): 103 | if id: 104 | queryset = self.get_queryset(id) 105 | if queryset: 106 | queryset.update(**dict([("set__%s" % key, value) for key, value in self.get_flat_arguments().items()])) 107 | else: 108 | queryset = self._generate_model(**self.get_flat_arguments()) 109 | if queryset: 110 | queryset.save() 111 | self.set_header("Content-Type", "application/json") 112 | self.write(self.serialize(queryset)) 113 | self.finish() 114 | 115 | @report_on_fail 116 | @expose_method 117 | def delete(self, id=None): 118 | queryset = self.get_queryset(id) 119 | if queryset: 120 | queryset.delete() 121 | self.set_header("Content-Type", "application/json") 122 | self.write(self.serialize(queryset)) 123 | self.finish() -------------------------------------------------------------------------------- /airy/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/letolab/airy/4dc53bec4aa0cd9b983a0a626fecdf49d14bdf94/airy/core/__init__.py -------------------------------------------------------------------------------- /airy/core/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Airy admin script 3 | """ 4 | import sys 5 | import os 6 | import shutil 7 | 8 | def execute(): 9 | 10 | if len(sys.argv) <= 1: 11 | print "No command supplied." 12 | sys.exit(1) 13 | 14 | command = sys.argv[1] 15 | args = sys.argv[2:] 16 | 17 | if command == 'help': 18 | help() 19 | 20 | elif command == 'startproject': 21 | startproject(*args) 22 | 23 | elif command == 'startapp': 24 | startapp(*args) 25 | 26 | else: 27 | print "Error: unknown command '%s'" % command 28 | 29 | def help(): 30 | print 'Usage: python %s ' % sys.argv[0] 31 | print """ 32 | Available commands: 33 | 34 | startproject : 35 | 36 | Creates a new project in a folder named 37 | 38 | startapp : 39 | 40 | Creates a new app in a folder named 41 | 42 | help: 43 | 44 | Displays this help 45 | 46 | """ 47 | 48 | def startproject(project_name): 49 | admin_path = os.path.abspath(os.path.dirname(__file__)) 50 | skeleton_path = os.path.join(admin_path, '../skeleton/project') 51 | shutil.copytree(skeleton_path, os.path.join(os.getcwd(), project_name)) 52 | print "Created project '%s'." % project_name 53 | 54 | def startapp(app_name): 55 | admin_path = os.path.abspath(os.path.dirname(__file__)) 56 | skeleton_path = os.path.join(admin_path, '../skeleton/app') 57 | shutil.copytree(skeleton_path, os.path.join(os.getcwd(), app_name)) 58 | print "Created app '%s'." % app_name 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /airy/core/conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuring Airy projects. 3 | 4 | By default Airy parses ``settings.py`` file located in the project's directory. 5 | 6 | You may specify parameters either in the file or on command line when launching your 7 | project, for example: 8 | 9 | .. code-block:: console 10 | 11 | $ python manage.py runserver --port=80 12 | 13 | Most commonly used options: 14 | 15 | * port (default=8000) 16 | 17 | Run on the given port 18 | 19 | * host (default="127.0.0.1:8000") 20 | 21 | Bind to the given ip/port. 22 | 23 | * template_path (default="./templates") 24 | 25 | Path to the directory containing project templates. 26 | 27 | * static_path (default="./static") 28 | 29 | Path to your project's static files (Airy will serve it automatically on /static/) 30 | 31 | For a full list of options, see ``python manage.py help`` 32 | 33 | """ 34 | import tornado.options 35 | from tornado.options import define, options 36 | import os 37 | 38 | define("port", default=8000, help="Run on the given port", type=int) 39 | define("flash_policy_port", default=8043, help="Port to run Flash policy on (for older browsers)", type=int) 40 | define("flash_policy_file", default="./flashpolicy.xml", help="Flash policy file") 41 | define("secret", default='', help="Application secret") 42 | define("template_path", default="./templates", help="Template directory path") 43 | define("static_path", default="./static", help="Static directory path") 44 | define("app_title", default="Example Web App", help="Default app title") 45 | define("xsrf_cookies", default=True, help="Disable XSRF cookies") 46 | define("installed_apps", default=[], help="List of installed applications", multiple=True) 47 | define("host", default="127.0.0.1:8000") 48 | define("email_host", default="smtp.gmail.com") 49 | define("email_port", default=587) 50 | define("email_host_user", default="") 51 | define("email_host_password", default="") 52 | 53 | 54 | class Settings(object): 55 | def __init__(self, **entries): 56 | self.__dict__.update(entries) 57 | 58 | def __getattribute__(self, name): 59 | return object.__getattribute__(self, name.lower()) 60 | 61 | 62 | settings = Settings() 63 | 64 | def _ensure_defaults(dsettings, options, project_root, config_filename): 65 | 66 | # paths 67 | if options.template_path == './templates': 68 | dsettings['template_path'] = os.path.join(project_root, 'templates') 69 | if options.static_path == './static': 70 | dsettings['static_path'] = os.path.join(project_root, 'static') 71 | if options.flash_policy_file == './flashpolicy.xml': 72 | dsettings['flash_policy_file'] = os.path.join(project_root, 'flashpolicy.xml') 73 | dsettings['locale_paths'] = () 74 | 75 | # translation 76 | dsettings['use_i18n'] = True 77 | dsettings['use_l10n'] = True 78 | dsettings['language_code'] = 'en-us' 79 | dsettings['default_charset'] = 'utf-8' 80 | 81 | # email 82 | dsettings['email_backend'] = 'airy.core.mail.backends.smtp.EmailBackend' 83 | dsettings['email_host'] = 'localhost' 84 | dsettings['email_host_post'] = 25 85 | dsettings['email_host_user'] = '' 86 | dsettings['email_host_password'] = '' 87 | dsettings['email_use_tls'] = False 88 | dsettings['email_subject_prefix'] = '[Airy] ' 89 | dsettings['server_email'] = 'root@localhost' 90 | 91 | # settings 92 | dsettings['project_root'] = project_root 93 | dsettings['settings_module'] = config_filename.replace('/', '.')[:-3].strip('.') 94 | 95 | # formats 96 | dsettings['datetime_format'] = getattr(options, 'datetime_format', 'N j, Y, P') 97 | dsettings['time_format'] = getattr(options, 'time_format', 'P') 98 | dsettings['date_input_formats'] = getattr(options, 'date_input_formats', 99 | ('%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', '%b %d %Y', 100 | '%b %d, %Y', '%d %b %Y', '%d %b, %Y', '%B %d %Y', 101 | '%B %d, %Y', '%d %B %Y', '%d %B, %Y') 102 | ) 103 | 104 | return dsettings 105 | 106 | def _preconfigure(project_root, config_filename='settings.py', argv=[]): 107 | "Read config file, process command-line args" 108 | tornado.options.parse_command_line(argv) 109 | tornado.options.parse_config_file(os.path.join(project_root, config_filename)) 110 | 111 | dsettings = {} 112 | parsed_options = getattr(options, '_options', options) 113 | for key in parsed_options: 114 | dsettings[key] = getattr(options, key) 115 | 116 | _ensure_defaults(dsettings, options, project_root, config_filename) 117 | 118 | config = __import__('settings') 119 | for name in dir(config): 120 | if not name.startswith('_'): 121 | dsettings[name] = getattr(config, name) 122 | 123 | settings.__dict__.update(**dsettings) 124 | 125 | settings.socket_io_port = settings.port 126 | 127 | return dsettings 128 | 129 | 130 | -------------------------------------------------------------------------------- /airy/core/context_processors.py: -------------------------------------------------------------------------------- 1 | from airy.core import markup as markup_mod 2 | 3 | def markup(handler, **kwargs): 4 | return {'markup': markup_mod} 5 | 6 | -------------------------------------------------------------------------------- /airy/core/db.py: -------------------------------------------------------------------------------- 1 | from airy.core.conf import settings 2 | from mongoengine import * 3 | 4 | connect(getattr(settings, 'database_name', 'airy')) 5 | 6 | -------------------------------------------------------------------------------- /airy/core/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic decorators 3 | """ 4 | 5 | def bot_friendly(func): 6 | 7 | def wrapped(obj, *args, **kwargs): 8 | from lxml.html import document_fromstring, tostring, fromstring 9 | from lxml.etree import ParserError 10 | from airy.core.web import site 11 | 12 | def get_current_user(): 13 | return None 14 | 15 | def render(target, template_name, **kwargs): 16 | try: 17 | document = document_fromstring(obj.HTML) 18 | fragment = fromstring(handler.render_string(template_name, **kwargs)) 19 | document.get_element_by_id(target[1:]).insert(0, fragment) 20 | obj.HTML = tostring(document) 21 | except ParserError: 22 | pass 23 | 24 | def append(target, data): 25 | document = document_fromstring(obj.HTML) 26 | to = document.get_element_by_id(target[1:]) 27 | to.append(fromstring(data)) 28 | obj.HTML = tostring(document) 29 | 30 | def prepend(target, data): 31 | document = document_fromstring(obj.HTML) 32 | to = document.get_element_by_id(target[1:]) 33 | to.insert(0, fromstring(data)) 34 | obj.HTML = tostring(document) 35 | 36 | def redirect(url): 37 | super(obj.__class__, obj).redirect(url) 38 | 39 | def remove(target): 40 | document = document_fromstring(obj.HTML) 41 | fragment = document.get_element_by_id(target[1:]) 42 | document.remove(fragment) 43 | obj.HTML = tostring(document) 44 | 45 | def set_title(text): 46 | document = document_fromstring(obj.HTML) 47 | document.findall('.//title')[0].text = text 48 | obj.HTML = tostring(document) 49 | 50 | def set_meta_description(text): 51 | document = document_fromstring(obj.HTML) 52 | try: 53 | document.findall('.//head/meta[@name="description"]')[0].attrib['content'] = text 54 | except IndexError: 55 | pass 56 | obj.HTML = tostring(document) 57 | 58 | if obj.is_robot(): 59 | obj.HTML = obj.render_string("page.html") 60 | uri = obj.request.uri 61 | arguments = obj.request.arguments 62 | handler, hargs, hkwargs = site.resolve_url(uri, None, arguments) 63 | handler.get_current_user = get_current_user 64 | handler.append = append 65 | handler.redirect = redirect 66 | handler.remove = remove 67 | handler.render = render 68 | handler.prepend = prepend 69 | handler.set_title = set_title 70 | handler.set_meta_description = set_meta_description 71 | handler.get(*hargs, **hkwargs) 72 | 73 | HTML = obj.HTML 74 | return obj.finish(HTML) 75 | return func(obj, *args, **kwargs) 76 | 77 | wrapped.__doc__ = func.__doc__ 78 | wrapped.__name__ = func.__name__ 79 | 80 | return wrapped 81 | -------------------------------------------------------------------------------- /airy/core/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Airy exceptions 3 | 4 | This was borrowed from Django 5 | """ 6 | 7 | class ObjectDoesNotExist(Exception): 8 | "The requested object does not exist" 9 | silent_variable_failure = True 10 | 11 | class MultipleObjectsReturned(Exception): 12 | "The query returned multiple objects when only one was expected." 13 | pass 14 | 15 | class SuspiciousOperation(Exception): 16 | "The user did something suspicious" 17 | pass 18 | 19 | class PermissionDenied(Exception): 20 | "The user did not have permission to do that" 21 | pass 22 | 23 | class ViewDoesNotExist(Exception): 24 | "The requested view does not exist" 25 | pass 26 | 27 | class MiddlewareNotUsed(Exception): 28 | "This middleware is not used in this server configuration" 29 | pass 30 | 31 | class ImproperlyConfigured(Exception): 32 | "Django is somehow improperly configured" 33 | pass 34 | 35 | class FieldError(Exception): 36 | """Some kind of problem with a model field.""" 37 | pass 38 | 39 | class Http404(Exception): 40 | "Requested page does not exist" 41 | pass 42 | 43 | NON_FIELD_ERRORS = '__all__' 44 | class ValidationError(Exception): 45 | """An error while validating data.""" 46 | def __init__(self, message, code=None, params=None): 47 | import operator 48 | from airy.utils.encoding import force_unicode 49 | """ 50 | ValidationError can be passed any object that can be printed (usually 51 | a string), a list of objects or a dictionary. 52 | """ 53 | if isinstance(message, dict): 54 | self.message_dict = message 55 | # Reduce each list of messages into a single list. 56 | message = reduce(operator.add, message.values()) 57 | 58 | if isinstance(message, list): 59 | self.messages = [force_unicode(msg) for msg in message] 60 | else: 61 | self.code = code 62 | self.params = params 63 | message = force_unicode(message) 64 | self.messages = [message] 65 | 66 | def __str__(self): 67 | # This is needed because, without a __str__(), printing an exception 68 | # instance would result in this: 69 | # AttributeError: ValidationError instance has no attribute 'args' 70 | # See http://www.python.org/doc/current/tut/node10.html#handling 71 | if hasattr(self, 'message_dict'): 72 | return repr(self.message_dict) 73 | return repr(self.messages) 74 | 75 | def __repr__(self): 76 | if hasattr(self, 'message_dict'): 77 | return 'ValidationError(%s)' % repr(self.message_dict) 78 | return 'ValidationError(%s)' % repr(self.messages) 79 | 80 | def update_error_dict(self, error_dict): 81 | if hasattr(self, 'message_dict'): 82 | if error_dict: 83 | for k, v in self.message_dict.items(): 84 | error_dict.setdefault(k, []).extend(v) 85 | else: 86 | error_dict = self.message_dict 87 | else: 88 | error_dict[NON_FIELD_ERRORS] = self.messages 89 | return error_dict 90 | 91 | -------------------------------------------------------------------------------- /airy/core/files/__init__.py: -------------------------------------------------------------------------------- 1 | from airy.core.files.base import File 2 | -------------------------------------------------------------------------------- /airy/core/files/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | try: 3 | from cStringIO import StringIO 4 | except ImportError: 5 | from StringIO import StringIO 6 | 7 | from airy.utils.encoding import smart_str, smart_unicode 8 | from airy.core.files.utils import FileProxyMixin 9 | 10 | class File(FileProxyMixin): 11 | DEFAULT_CHUNK_SIZE = 64 * 2**10 12 | 13 | def __init__(self, file, name=None): 14 | self.file = file 15 | if name is None: 16 | name = getattr(file, 'name', None) 17 | self.name = name 18 | self.mode = getattr(file, 'mode', None) 19 | 20 | def __str__(self): 21 | return smart_str(self.name or '') 22 | 23 | def __unicode__(self): 24 | return smart_unicode(self.name or u'') 25 | 26 | def __repr__(self): 27 | return "<%s: %s>" % (self.__class__.__name__, self or "None") 28 | 29 | def __nonzero__(self): 30 | return bool(self.name) 31 | 32 | def __len__(self): 33 | return self.size 34 | 35 | def _get_size(self): 36 | if not hasattr(self, '_size'): 37 | if hasattr(self.file, 'size'): 38 | self._size = self.file.size 39 | elif os.path.exists(self.file.name): 40 | self._size = os.path.getsize(self.file.name) 41 | else: 42 | raise AttributeError("Unable to determine the file's size.") 43 | return self._size 44 | 45 | def _set_size(self, size): 46 | self._size = size 47 | 48 | size = property(_get_size, _set_size) 49 | 50 | def _get_closed(self): 51 | return not self.file or self.file.closed 52 | closed = property(_get_closed) 53 | 54 | def chunks(self, chunk_size=None): 55 | """ 56 | Read the file and yield chucks of ``chunk_size`` bytes (defaults to 57 | ``UploadedFile.DEFAULT_CHUNK_SIZE``). 58 | """ 59 | if not chunk_size: 60 | chunk_size = self.DEFAULT_CHUNK_SIZE 61 | 62 | if hasattr(self, 'seek'): 63 | self.seek(0) 64 | # Assume the pointer is at zero... 65 | counter = self.size 66 | 67 | while counter > 0: 68 | yield self.read(chunk_size) 69 | counter -= chunk_size 70 | 71 | def multiple_chunks(self, chunk_size=None): 72 | """ 73 | Returns ``True`` if you can expect multiple chunks. 74 | 75 | NB: If a particular file representation is in memory, subclasses should 76 | always return ``False`` -- there's no good reason to read from memory in 77 | chunks. 78 | """ 79 | if not chunk_size: 80 | chunk_size = self.DEFAULT_CHUNK_SIZE 81 | return self.size > chunk_size 82 | 83 | def __iter__(self): 84 | # Iterate over this file-like object by newlines 85 | buffer_ = None 86 | for chunk in self.chunks(): 87 | chunk_buffer = StringIO(chunk) 88 | 89 | for line in chunk_buffer: 90 | if buffer_: 91 | line = buffer_ + line 92 | buffer_ = None 93 | 94 | # If this is the end of a line, yield 95 | # otherwise, wait for the next round 96 | if line[-1] in ('\n', '\r'): 97 | yield line 98 | else: 99 | buffer_ = line 100 | 101 | if buffer_ is not None: 102 | yield buffer_ 103 | 104 | def __enter__(self): 105 | return self 106 | 107 | def __exit__(self, exc_type, exc_value, tb): 108 | self.close() 109 | 110 | def open(self, mode=None): 111 | if not self.closed: 112 | self.seek(0) 113 | elif self.name and os.path.exists(self.name): 114 | self.file = open(self.name, mode or self.mode) 115 | else: 116 | raise ValueError("The file cannot be reopened.") 117 | 118 | def close(self): 119 | self.file.close() 120 | 121 | class ContentFile(File): 122 | """ 123 | A File-like object that takes just raw content, rather than an actual file. 124 | """ 125 | def __init__(self, content): 126 | content = content or '' 127 | super(ContentFile, self).__init__(StringIO(content)) 128 | self.size = len(content) 129 | 130 | def __str__(self): 131 | return 'Raw content' 132 | 133 | def __nonzero__(self): 134 | return True 135 | 136 | def open(self, mode=None): 137 | self.seek(0) 138 | 139 | def close(self): 140 | pass 141 | -------------------------------------------------------------------------------- /airy/core/files/images.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility functions for handling images. 3 | 4 | Requires PIL, as you might imagine. 5 | """ 6 | 7 | from airy.core.files import File 8 | 9 | class ImageFile(File): 10 | """ 11 | A mixin for use alongside airy.core.files.base.File, which provides 12 | additional features for dealing with images. 13 | """ 14 | def _get_width(self): 15 | return self._get_image_dimensions()[0] 16 | width = property(_get_width) 17 | 18 | def _get_height(self): 19 | return self._get_image_dimensions()[1] 20 | height = property(_get_height) 21 | 22 | def _get_image_dimensions(self): 23 | if not hasattr(self, '_dimensions_cache'): 24 | close = self.closed 25 | self.open() 26 | self._dimensions_cache = get_image_dimensions(self, close=close) 27 | return self._dimensions_cache 28 | 29 | def get_image_dimensions(file_or_path, close=False): 30 | """ 31 | Returns the (width, height) of an image, given an open file or a path. Set 32 | 'close' to True to close the file at the end if it is initially in an open 33 | state. 34 | """ 35 | # Try to import PIL in either of the two ways it can end up installed. 36 | try: 37 | from PIL import ImageFile as PILImageFile 38 | except ImportError: 39 | import ImageFile as PILImageFile 40 | 41 | p = PILImageFile.Parser() 42 | if hasattr(file_or_path, 'read'): 43 | file = file_or_path 44 | file_pos = file.tell() 45 | file.seek(0) 46 | else: 47 | file = open(file_or_path, 'rb') 48 | close = True 49 | try: 50 | while 1: 51 | data = file.read(1024) 52 | if not data: 53 | break 54 | p.feed(data) 55 | if p.image: 56 | return p.image.size 57 | return None 58 | finally: 59 | if close: 60 | file.close() 61 | else: 62 | file.seek(file_pos) 63 | -------------------------------------------------------------------------------- /airy/core/files/locks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Portable file locking utilities. 3 | 4 | Based partially on example by Jonathan Feignberg in the Python 5 | Cookbook, licensed under the Python Software License. 6 | 7 | http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/65203 8 | 9 | Example Usage:: 10 | 11 | >>> from airy.core.files import locks 12 | >>> f = open('./file', 'wb') 13 | >>> locks.lock(f, locks.LOCK_EX) 14 | >>> f.write('Django') 15 | >>> f.close() 16 | """ 17 | 18 | __all__ = ('LOCK_EX','LOCK_SH','LOCK_NB','lock','unlock') 19 | 20 | system_type = None 21 | 22 | try: 23 | import win32con 24 | import win32file 25 | import pywintypes 26 | LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK 27 | LOCK_SH = 0 28 | LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY 29 | __overlapped = pywintypes.OVERLAPPED() 30 | system_type = 'nt' 31 | except (ImportError, AttributeError): 32 | pass 33 | 34 | try: 35 | import fcntl 36 | LOCK_EX = fcntl.LOCK_EX 37 | LOCK_SH = fcntl.LOCK_SH 38 | LOCK_NB = fcntl.LOCK_NB 39 | system_type = 'posix' 40 | except (ImportError, AttributeError): 41 | pass 42 | 43 | def fd(f): 44 | """Get a filedescriptor from something which could be a file or an fd.""" 45 | return hasattr(f, 'fileno') and f.fileno() or f 46 | 47 | if system_type == 'nt': 48 | def lock(file, flags): 49 | hfile = win32file._get_osfhandle(fd(file)) 50 | win32file.LockFileEx(hfile, flags, 0, -0x10000, __overlapped) 51 | 52 | def unlock(file): 53 | hfile = win32file._get_osfhandle(fd(file)) 54 | win32file.UnlockFileEx(hfile, 0, -0x10000, __overlapped) 55 | elif system_type == 'posix': 56 | def lock(file, flags): 57 | fcntl.lockf(fd(file), flags) 58 | 59 | def unlock(file): 60 | fcntl.lockf(fd(file), fcntl.LOCK_UN) 61 | else: 62 | # File locking is not supported. 63 | LOCK_EX = LOCK_SH = LOCK_NB = None 64 | 65 | # Dummy functions that don't do anything. 66 | def lock(file, flags): 67 | pass 68 | 69 | def unlock(file): 70 | pass 71 | -------------------------------------------------------------------------------- /airy/core/files/move.py: -------------------------------------------------------------------------------- 1 | """ 2 | Move a file in the safest way possible:: 3 | 4 | >>> from airy.core.files.move import file_move_safe 5 | >>> file_move_safe("/tmp/old_file", "/tmp/new_file") 6 | """ 7 | 8 | import os 9 | from airy.core.files import locks 10 | 11 | try: 12 | from shutil import copystat 13 | except ImportError: 14 | import stat 15 | def copystat(src, dst): 16 | """Copy all stat info (mode bits, atime and mtime) from src to dst""" 17 | st = os.stat(src) 18 | mode = stat.S_IMODE(st.st_mode) 19 | if hasattr(os, 'utime'): 20 | os.utime(dst, (st.st_atime, st.st_mtime)) 21 | if hasattr(os, 'chmod'): 22 | os.chmod(dst, mode) 23 | 24 | __all__ = ['file_move_safe'] 25 | 26 | def _samefile(src, dst): 27 | # Macintosh, Unix. 28 | if hasattr(os.path,'samefile'): 29 | try: 30 | return os.path.samefile(src, dst) 31 | except OSError: 32 | return False 33 | 34 | # All other platforms: check for same pathname. 35 | return (os.path.normcase(os.path.abspath(src)) == 36 | os.path.normcase(os.path.abspath(dst))) 37 | 38 | def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_overwrite=False): 39 | """ 40 | Moves a file from one location to another in the safest way possible. 41 | 42 | First, tries ``os.rename``, which is simple but will break across filesystems. 43 | If that fails, streams manually from one file to another in pure Python. 44 | 45 | If the destination file exists and ``allow_overwrite`` is ``False``, this 46 | function will throw an ``IOError``. 47 | """ 48 | 49 | # There's no reason to move if we don't have to. 50 | if _samefile(old_file_name, new_file_name): 51 | return 52 | 53 | try: 54 | os.rename(old_file_name, new_file_name) 55 | return 56 | except OSError: 57 | # This will happen with os.rename if moving to another filesystem 58 | # or when moving opened files on certain operating systems 59 | pass 60 | 61 | # first open the old file, so that it won't go away 62 | old_file = open(old_file_name, 'rb') 63 | try: 64 | # now open the new file, not forgetting allow_overwrite 65 | fd = os.open(new_file_name, os.O_WRONLY | os.O_CREAT | getattr(os, 'O_BINARY', 0) | 66 | (not allow_overwrite and os.O_EXCL or 0)) 67 | try: 68 | locks.lock(fd, locks.LOCK_EX) 69 | current_chunk = None 70 | while current_chunk != '': 71 | current_chunk = old_file.read(chunk_size) 72 | os.write(fd, current_chunk) 73 | finally: 74 | locks.unlock(fd) 75 | os.close(fd) 76 | finally: 77 | old_file.close() 78 | copystat(old_file_name, new_file_name) 79 | 80 | try: 81 | os.remove(old_file_name) 82 | except OSError, e: 83 | # Certain operating systems (Cygwin and Windows) 84 | # fail when deleting opened files, ignore it. (For the 85 | # systems where this happens, temporary files will be auto-deleted 86 | # on close anyway.) 87 | if getattr(e, 'winerror', 0) != 32 and getattr(e, 'errno', 0) != 13: 88 | raise 89 | -------------------------------------------------------------------------------- /airy/core/files/temp.py: -------------------------------------------------------------------------------- 1 | """ 2 | The temp module provides a NamedTemporaryFile that can be re-opened on any 3 | platform. Most platforms use the standard Python tempfile.TemporaryFile class, 4 | but MS Windows users are given a custom class. 5 | 6 | This is needed because in Windows NT, the default implementation of 7 | NamedTemporaryFile uses the O_TEMPORARY flag, and thus cannot be reopened [1]. 8 | 9 | 1: http://mail.python.org/pipermail/python-list/2005-December/359474.html 10 | """ 11 | 12 | import os 13 | import tempfile 14 | from airy.core.files.utils import FileProxyMixin 15 | 16 | __all__ = ('NamedTemporaryFile', 'gettempdir',) 17 | 18 | if os.name == 'nt': 19 | class TemporaryFile(FileProxyMixin): 20 | """ 21 | Temporary file object constructor that works in Windows and supports 22 | reopening of the temporary file in windows. 23 | """ 24 | def __init__(self, mode='w+b', bufsize=-1, suffix='', prefix='', 25 | dir=None): 26 | fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix, 27 | dir=dir) 28 | self.name = name 29 | self.file = os.fdopen(fd, mode, bufsize) 30 | self.close_called = False 31 | 32 | # Because close can be called during shutdown 33 | # we need to cache os.unlink and access it 34 | # as self.unlink only 35 | unlink = os.unlink 36 | 37 | def close(self): 38 | if not self.close_called: 39 | self.close_called = True 40 | try: 41 | self.file.close() 42 | except (OSError, IOError): 43 | pass 44 | try: 45 | self.unlink(self.name) 46 | except (OSError): 47 | pass 48 | 49 | def __del__(self): 50 | self.close() 51 | 52 | NamedTemporaryFile = TemporaryFile 53 | else: 54 | NamedTemporaryFile = tempfile.NamedTemporaryFile 55 | 56 | gettempdir = tempfile.gettempdir 57 | -------------------------------------------------------------------------------- /airy/core/files/uploadedfile.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes representing uploaded files. 3 | """ 4 | 5 | import os 6 | try: 7 | from cStringIO import StringIO 8 | except ImportError: 9 | from StringIO import StringIO 10 | 11 | from airy.core.conf import settings 12 | from airy.core.files.base import File 13 | from airy.core.files import temp as tempfile 14 | from airy.utils.encoding import smart_str 15 | 16 | __all__ = ('UploadedFile', 'TemporaryUploadedFile', 'InMemoryUploadedFile', 17 | 'SimpleUploadedFile') 18 | 19 | class UploadedFile(File): 20 | """ 21 | A abstract uploaded file (``TemporaryUploadedFile`` and 22 | ``InMemoryUploadedFile`` are the built-in concrete subclasses). 23 | 24 | An ``UploadedFile`` object behaves somewhat like a file object and 25 | represents some file data that the user submitted with a form. 26 | """ 27 | DEFAULT_CHUNK_SIZE = 64 * 2**10 28 | 29 | def __init__(self, file=None, name=None, content_type=None, size=None, charset=None): 30 | super(UploadedFile, self).__init__(file, name) 31 | self.size = size 32 | self.content_type = content_type 33 | self.charset = charset 34 | 35 | def __repr__(self): 36 | return "<%s: %s (%s)>" % ( 37 | self.__class__.__name__, smart_str(self.name), self.content_type) 38 | 39 | def _get_name(self): 40 | return self._name 41 | 42 | def _set_name(self, name): 43 | # Sanitize the file name so that it can't be dangerous. 44 | if name is not None: 45 | # Just use the basename of the file -- anything else is dangerous. 46 | name = os.path.basename(name) 47 | 48 | # File names longer than 255 characters can cause problems on older OSes. 49 | if len(name) > 255: 50 | name, ext = os.path.splitext(name) 51 | name = name[:255 - len(ext)] + ext 52 | 53 | self._name = name 54 | 55 | name = property(_get_name, _set_name) 56 | 57 | class TemporaryUploadedFile(UploadedFile): 58 | """ 59 | A file uploaded to a temporary location (i.e. stream-to-disk). 60 | """ 61 | def __init__(self, name, content_type, size, charset): 62 | if settings.FILE_UPLOAD_TEMP_DIR: 63 | file = tempfile.NamedTemporaryFile(suffix='.upload', 64 | dir=settings.FILE_UPLOAD_TEMP_DIR) 65 | else: 66 | file = tempfile.NamedTemporaryFile(suffix='.upload') 67 | super(TemporaryUploadedFile, self).__init__(file, name, content_type, size, charset) 68 | 69 | def temporary_file_path(self): 70 | """ 71 | Returns the full path of this file. 72 | """ 73 | return self.file.name 74 | 75 | def close(self): 76 | try: 77 | return self.file.close() 78 | except OSError, e: 79 | if e.errno != 2: 80 | # Means the file was moved or deleted before the tempfile 81 | # could unlink it. Still sets self.file.close_called and 82 | # calls self.file.file.close() before the exception 83 | raise 84 | 85 | class InMemoryUploadedFile(UploadedFile): 86 | """ 87 | A file uploaded into memory (i.e. stream-to-memory). 88 | """ 89 | def __init__(self, file, field_name, name, content_type, size, charset): 90 | super(InMemoryUploadedFile, self).__init__(file, name, content_type, size, charset) 91 | self.field_name = field_name 92 | 93 | def open(self, mode=None): 94 | self.file.seek(0) 95 | 96 | def close(self): 97 | pass 98 | 99 | def chunks(self, chunk_size=None): 100 | self.file.seek(0) 101 | yield self.read() 102 | 103 | def multiple_chunks(self, chunk_size=None): 104 | # Since it's in memory, we'll never have multiple chunks. 105 | return False 106 | 107 | 108 | class SimpleUploadedFile(InMemoryUploadedFile): 109 | """ 110 | A simple representation of a file, which just has content, size, and a name. 111 | """ 112 | def __init__(self, name, content, content_type='text/plain'): 113 | content = content or '' 114 | super(SimpleUploadedFile, self).__init__(StringIO(content), None, name, 115 | content_type, len(content), None) 116 | 117 | def from_dict(cls, file_dict): 118 | """ 119 | Creates a SimpleUploadedFile object from 120 | a dictionary object with the following keys: 121 | - filename 122 | - content-type 123 | - content 124 | """ 125 | return cls(file_dict['filename'], 126 | file_dict['content'], 127 | file_dict.get('content-type', 'text/plain')) 128 | from_dict = classmethod(from_dict) 129 | -------------------------------------------------------------------------------- /airy/core/files/utils.py: -------------------------------------------------------------------------------- 1 | class FileProxyMixin(object): 2 | """ 3 | A mixin class used to forward file methods to an underlaying file 4 | object. The internal file object has to be called "file":: 5 | 6 | class FileProxy(FileProxyMixin): 7 | def __init__(self, file): 8 | self.file = file 9 | """ 10 | 11 | encoding = property(lambda self: self.file.encoding) 12 | fileno = property(lambda self: self.file.fileno) 13 | flush = property(lambda self: self.file.flush) 14 | isatty = property(lambda self: self.file.isatty) 15 | newlines = property(lambda self: self.file.newlines) 16 | read = property(lambda self: self.file.read) 17 | readinto = property(lambda self: self.file.readinto) 18 | readline = property(lambda self: self.file.readline) 19 | readlines = property(lambda self: self.file.readlines) 20 | seek = property(lambda self: self.file.seek) 21 | softspace = property(lambda self: self.file.softspace) 22 | tell = property(lambda self: self.file.tell) 23 | truncate = property(lambda self: self.file.truncate) 24 | write = property(lambda self: self.file.write) 25 | writelines = property(lambda self: self.file.writelines) 26 | xreadlines = property(lambda self: self.file.xreadlines) 27 | 28 | def __iter__(self): 29 | return iter(self.file) 30 | -------------------------------------------------------------------------------- /airy/core/mail/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tools for sending email. 3 | """ 4 | 5 | from airy.core.conf import settings 6 | from airy.core.exceptions import ImproperlyConfigured 7 | from airy.utils.importlib import import_module 8 | 9 | # Imported for backwards compatibility, and for the sake 10 | # of a cleaner namespace. These symbols used to be in 11 | # django/core/mail.py before the introduction of email 12 | # backends and the subsequent reorganization (See #10355) 13 | from airy.core.mail.utils import CachedDnsName, DNS_NAME 14 | from airy.core.mail.message import ( 15 | EmailMessage, EmailMultiAlternatives, 16 | SafeMIMEText, SafeMIMEMultipart, 17 | DEFAULT_ATTACHMENT_MIME_TYPE, make_msgid, 18 | BadHeaderError, forbid_multi_line_headers) 19 | 20 | 21 | def get_connection(backend=None, fail_silently=False, **kwds): 22 | """Load an email backend and return an instance of it. 23 | 24 | If backend is None (default) settings.EMAIL_BACKEND is used. 25 | 26 | Both fail_silently and other keyword arguments are used in the 27 | constructor of the backend. 28 | """ 29 | path = backend or settings.email_backend 30 | try: 31 | mod_name, klass_name = path.rsplit('.', 1) 32 | mod = import_module(mod_name) 33 | except ImportError, e: 34 | raise ImproperlyConfigured(('Error importing email backend module %s: "%s"' 35 | % (mod_name, e))) 36 | try: 37 | klass = getattr(mod, klass_name) 38 | except AttributeError: 39 | raise ImproperlyConfigured(('Module "%s" does not define a ' 40 | '"%s" class' % (mod_name, klass_name))) 41 | return klass(fail_silently=fail_silently, **kwds) 42 | 43 | 44 | def send_mail(subject, message, from_email, recipient_list, 45 | fail_silently=False, auth_user=None, auth_password=None, 46 | connection=None): 47 | """ 48 | Easy wrapper for sending a single message to a recipient list. All members 49 | of the recipient list will see the other recipients in the 'To' field. 50 | 51 | If auth_user is None, the EMAIL_HOST_USER setting is used. 52 | If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. 53 | 54 | Note: The API for this method is frozen. New code wanting to extend the 55 | functionality should use the EmailMessage class directly. 56 | """ 57 | connection = connection or get_connection(username=auth_user, 58 | password=auth_password, 59 | fail_silently=fail_silently) 60 | return EmailMessage(subject, message, from_email, recipient_list, 61 | connection=connection).send() 62 | 63 | 64 | def send_mail_multipart(subject, message, from_email, recipient_list, 65 | fail_silently=False, connection=None, 66 | html_message=None, attachments=[]): 67 | """Sends a multipart message.""" 68 | if not settings.ADMINS: 69 | return 70 | mail = EmailMultiAlternatives(subject, 71 | message, from_email, recipient_list, 72 | connection=connection) 73 | if html_message: 74 | mail.attach_alternative(html_message, 'text/html') 75 | for filepath in attachments: 76 | mail.attach_file(filepath) 77 | mail.send(fail_silently=fail_silently) 78 | 79 | 80 | def send_mass_mail(datatuple, fail_silently=False, auth_user=None, 81 | auth_password=None, connection=None): 82 | """ 83 | Given a datatuple of (subject, message, from_email, recipient_list), sends 84 | each message to each recipient list. Returns the number of emails sent. 85 | 86 | If from_email is None, the DEFAULT_FROM_EMAIL setting is used. 87 | If auth_user and auth_password are set, they're used to log in. 88 | If auth_user is None, the EMAIL_HOST_USER setting is used. 89 | If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. 90 | 91 | Note: The API for this method is frozen. New code wanting to extend the 92 | functionality should use the EmailMessage class directly. 93 | """ 94 | connection = connection or get_connection(username=auth_user, 95 | password=auth_password, 96 | fail_silently=fail_silently) 97 | messages = [EmailMessage(subject, message, sender, recipient, 98 | connection=connection) 99 | for subject, message, sender, recipient in datatuple] 100 | return connection.send_messages(messages) 101 | 102 | 103 | def mail_admins(subject, message, fail_silently=False, connection=None, 104 | html_message=None): 105 | """Sends a message to the admins, as defined by the ADMINS setting.""" 106 | if not settings.ADMINS: 107 | return 108 | mail = EmailMultiAlternatives(u'%s%s' % (settings.email_subject_prefix, subject), 109 | message, settings.server_email, [a[1] for a in settings.admins], 110 | connection=connection) 111 | if html_message: 112 | mail.attach_alternative(html_message, 'text/html') 113 | mail.send(fail_silently=fail_silently) 114 | 115 | 116 | def mail_managers(subject, message, fail_silently=False, connection=None, 117 | html_message=None): 118 | """Sends a message to the managers, as defined by the MANAGERS setting.""" 119 | if not settings.MANAGERS: 120 | return 121 | mail = EmailMultiAlternatives(u'%s%s' % (settings.email_subject_prefix, subject), 122 | message, settings.server_email, [a[1] for a in settings.managers], 123 | connection=connection) 124 | if html_message: 125 | mail.attach_alternative(html_message, 'text/html') 126 | mail.send(fail_silently=fail_silently) 127 | -------------------------------------------------------------------------------- /airy/core/mail/backends/__init__.py: -------------------------------------------------------------------------------- 1 | # Mail backends shipped with Django. 2 | -------------------------------------------------------------------------------- /airy/core/mail/backends/base.py: -------------------------------------------------------------------------------- 1 | """Base email backend class.""" 2 | 3 | class BaseEmailBackend(object): 4 | """ 5 | Base class for email backend implementations. 6 | 7 | Subclasses must at least overwrite send_messages(). 8 | """ 9 | def __init__(self, fail_silently=False, **kwargs): 10 | self.fail_silently = fail_silently 11 | 12 | def open(self): 13 | """Open a network connection. 14 | 15 | This method can be overwritten by backend implementations to 16 | open a network connection. 17 | 18 | It's up to the backend implementation to track the status of 19 | a network connection if it's needed by the backend. 20 | 21 | This method can be called by applications to force a single 22 | network connection to be used when sending mails. See the 23 | send_messages() method of the SMTP backend for a reference 24 | implementation. 25 | 26 | The default implementation does nothing. 27 | """ 28 | pass 29 | 30 | def close(self): 31 | """Close a network connection.""" 32 | pass 33 | 34 | def send_messages(self, email_messages): 35 | """ 36 | Sends one or more EmailMessage objects and returns the number of email 37 | messages sent. 38 | """ 39 | raise NotImplementedError 40 | -------------------------------------------------------------------------------- /airy/core/mail/backends/console.py: -------------------------------------------------------------------------------- 1 | """ 2 | Email backend that writes messages to console instead of sending them. 3 | """ 4 | import sys 5 | import threading 6 | 7 | from airy.core.mail.backends.base import BaseEmailBackend 8 | 9 | class EmailBackend(BaseEmailBackend): 10 | def __init__(self, *args, **kwargs): 11 | self.stream = kwargs.pop('stream', sys.stdout) 12 | self._lock = threading.RLock() 13 | super(EmailBackend, self).__init__(*args, **kwargs) 14 | 15 | def send_messages(self, email_messages): 16 | """Write all messages to the stream in a thread-safe way.""" 17 | if not email_messages: 18 | return 19 | self._lock.acquire() 20 | try: 21 | stream_created = self.open() 22 | for message in email_messages: 23 | self.stream.write('%s\n' % message.message().as_string()) 24 | self.stream.write('-'*79) 25 | self.stream.write('\n') 26 | self.stream.flush() # flush after each message 27 | if stream_created: 28 | self.close() 29 | except: 30 | if not self.fail_silently: 31 | raise 32 | finally: 33 | self._lock.release() 34 | return len(email_messages) 35 | -------------------------------------------------------------------------------- /airy/core/mail/backends/dummy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dummy email backend that does nothing. 3 | """ 4 | 5 | from airy.core.mail.backends.base import BaseEmailBackend 6 | 7 | class EmailBackend(BaseEmailBackend): 8 | def send_messages(self, email_messages): 9 | return len(email_messages) 10 | -------------------------------------------------------------------------------- /airy/core/mail/backends/filebased.py: -------------------------------------------------------------------------------- 1 | """Email backend that writes messages to a file.""" 2 | 3 | import datetime 4 | import os 5 | 6 | from airy.core.conf import settings 7 | from airy.core.exceptions import ImproperlyConfigured 8 | from airy.core.mail.backends.console import EmailBackend as ConsoleEmailBackend 9 | 10 | class EmailBackend(ConsoleEmailBackend): 11 | def __init__(self, *args, **kwargs): 12 | self._fname = None 13 | if 'file_path' in kwargs: 14 | self.file_path = kwargs.pop('file_path') 15 | else: 16 | self.file_path = getattr(settings, 'EMAIL_FILE_PATH',None) 17 | # Make sure self.file_path is a string. 18 | if not isinstance(self.file_path, basestring): 19 | raise ImproperlyConfigured('Path for saving emails is invalid: %r' % self.file_path) 20 | self.file_path = os.path.abspath(self.file_path) 21 | # Make sure that self.file_path is an directory if it exists. 22 | if os.path.exists(self.file_path) and not os.path.isdir(self.file_path): 23 | raise ImproperlyConfigured('Path for saving email messages exists, but is not a directory: %s' % self.file_path) 24 | # Try to create it, if it not exists. 25 | elif not os.path.exists(self.file_path): 26 | try: 27 | os.makedirs(self.file_path) 28 | except OSError, err: 29 | raise ImproperlyConfigured('Could not create directory for saving email messages: %s (%s)' % (self.file_path, err)) 30 | # Make sure that self.file_path is writable. 31 | if not os.access(self.file_path, os.W_OK): 32 | raise ImproperlyConfigured('Could not write to directory: %s' % self.file_path) 33 | # Finally, call super(). 34 | # Since we're using the console-based backend as a base, 35 | # force the stream to be None, so we don't default to stdout 36 | kwargs['stream'] = None 37 | super(EmailBackend, self).__init__(*args, **kwargs) 38 | 39 | def _get_filename(self): 40 | """Return a unique file name.""" 41 | if self._fname is None: 42 | timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") 43 | fname = "%s-%s.log" % (timestamp, abs(id(self))) 44 | self._fname = os.path.join(self.file_path, fname) 45 | return self._fname 46 | 47 | def open(self): 48 | if self.stream is None: 49 | self.stream = open(self._get_filename(), 'a') 50 | return True 51 | return False 52 | 53 | def close(self): 54 | try: 55 | if self.stream is not None: 56 | self.stream.close() 57 | finally: 58 | self.stream = None 59 | 60 | -------------------------------------------------------------------------------- /airy/core/mail/backends/locmem.py: -------------------------------------------------------------------------------- 1 | """ 2 | Backend for test environment. 3 | """ 4 | 5 | from airy.core import mail 6 | from airy.core.mail.backends.base import BaseEmailBackend 7 | 8 | class EmailBackend(BaseEmailBackend): 9 | """A email backend for use during test sessions. 10 | 11 | The test connection stores email messages in a dummy outbox, 12 | rather than sending them out on the wire. 13 | 14 | The dummy outbox is accessible through the outbox instance attribute. 15 | """ 16 | def __init__(self, *args, **kwargs): 17 | super(EmailBackend, self).__init__(*args, **kwargs) 18 | if not hasattr(mail, 'outbox'): 19 | mail.outbox = [] 20 | 21 | def send_messages(self, messages): 22 | """Redirect messages to the dummy outbox""" 23 | mail.outbox.extend(messages) 24 | return len(messages) 25 | -------------------------------------------------------------------------------- /airy/core/mail/backends/smtp.py: -------------------------------------------------------------------------------- 1 | """SMTP email backend class.""" 2 | import smtplib 3 | import socket 4 | import threading 5 | 6 | from airy.core.conf import settings 7 | from airy.core.mail.backends.base import BaseEmailBackend 8 | from airy.core.mail.utils import DNS_NAME 9 | from airy.core.mail.message import sanitize_address 10 | 11 | 12 | class EmailBackend(BaseEmailBackend): 13 | """ 14 | A wrapper that manages the SMTP network connection. 15 | """ 16 | def __init__(self, host=None, port=None, username=None, password=None, 17 | use_tls=None, fail_silently=False, **kwargs): 18 | super(EmailBackend, self).__init__(fail_silently=fail_silently) 19 | self.host = host or settings.EMAIL_HOST 20 | self.port = port or settings.EMAIL_PORT 21 | if username is None: 22 | self.username = settings.EMAIL_HOST_USER 23 | else: 24 | self.username = username 25 | if password is None: 26 | self.password = settings.EMAIL_HOST_PASSWORD 27 | else: 28 | self.password = password 29 | if use_tls is None: 30 | self.use_tls = settings.EMAIL_USE_TLS 31 | else: 32 | self.use_tls = use_tls 33 | self.connection = None 34 | self._lock = threading.RLock() 35 | 36 | def open(self): 37 | """ 38 | Ensures we have a connection to the email server. Returns whether or 39 | not a new connection was required (True or False). 40 | """ 41 | if self.connection: 42 | # Nothing to do if the connection is already open. 43 | return False 44 | try: 45 | # If local_hostname is not specified, socket.getfqdn() gets used. 46 | # For performance, we use the cached FQDN for local_hostname. 47 | self.connection = smtplib.SMTP(self.host, self.port, 48 | local_hostname=DNS_NAME.get_fqdn()) 49 | if self.use_tls: 50 | self.connection.ehlo() 51 | self.connection.starttls() 52 | self.connection.ehlo() 53 | if self.username and self.password: 54 | self.connection.login(self.username, self.password) 55 | return True 56 | except: 57 | if not self.fail_silently: 58 | raise 59 | 60 | def close(self): 61 | """Closes the connection to the email server.""" 62 | try: 63 | try: 64 | self.connection.quit() 65 | except socket.sslerror: 66 | # This happens when calling quit() on a TLS connection 67 | # sometimes. 68 | self.connection.close() 69 | except: 70 | if self.fail_silently: 71 | return 72 | raise 73 | finally: 74 | self.connection = None 75 | 76 | def send_messages(self, email_messages): 77 | """ 78 | Sends one or more EmailMessage objects and returns the number of email 79 | messages sent. 80 | """ 81 | if not email_messages: 82 | return 83 | self._lock.acquire() 84 | try: 85 | new_conn_created = self.open() 86 | if not self.connection: 87 | # We failed silently on open(). 88 | # Trying to send would be pointless. 89 | return 90 | num_sent = 0 91 | for message in email_messages: 92 | sent = self._send(message) 93 | if sent: 94 | num_sent += 1 95 | if new_conn_created: 96 | self.close() 97 | finally: 98 | self._lock.release() 99 | return num_sent 100 | 101 | def _send(self, email_message): 102 | """A helper method that does the actual sending.""" 103 | if not email_message.recipients(): 104 | return False 105 | from_email = sanitize_address(email_message.from_email, email_message.encoding) 106 | recipients = [sanitize_address(addr, email_message.encoding) 107 | for addr in email_message.recipients()] 108 | try: 109 | self.connection.sendmail(from_email, recipients, 110 | email_message.message().as_string()) 111 | except: 112 | if not self.fail_silently: 113 | raise 114 | return False 115 | return True 116 | -------------------------------------------------------------------------------- /airy/core/mail/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Email message and email sending related helper functions. 3 | """ 4 | 5 | import socket 6 | 7 | 8 | # Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of 9 | # seconds, which slows down the restart of the server. 10 | class CachedDnsName(object): 11 | def __str__(self): 12 | return self.get_fqdn() 13 | 14 | def get_fqdn(self): 15 | if not hasattr(self, '_fqdn'): 16 | self._fqdn = socket.getfqdn() 17 | return self._fqdn 18 | 19 | DNS_NAME = CachedDnsName() 20 | -------------------------------------------------------------------------------- /airy/core/manager.py: -------------------------------------------------------------------------------- 1 | "Base engine" 2 | import tornado.web 3 | from tornado import template, autoreload 4 | from tornadio2 import SocketServer, TornadioRouter 5 | from airy.core.conf import settings, _preconfigure 6 | from airy.core import web 7 | import os 8 | 9 | AIRY_ROOT = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../') 10 | 11 | class CommandManager(dict): 12 | def __init__(self, *args, **kwargs): 13 | super(CommandManager, self).__init__(*args, **kwargs) 14 | for k,v in kwargs.items(): 15 | self[k] = v 16 | 17 | def load(self, project_root): 18 | for appname in settings.installed_apps: 19 | __import__(appname, fromlist=['%s.models'%appname]) 20 | 21 | command_manager = CommandManager() 22 | 23 | def execute(project_root, argv): 24 | _preconfigure(project_root, argv=argv[1:]) 25 | command_manager.load(project_root) 26 | 27 | if len(argv) <= 1: 28 | print 'Please provide a command.' 29 | elif argv[1] == 'run' or argv[1] == 'runserver': 30 | run(project_root) 31 | elif argv[1] == 'shell': 32 | shell() 33 | elif argv[1] == 'help': 34 | import tornado.options 35 | tornado.options.print_help() 36 | else: 37 | if argv[1] in command_manager: 38 | command_manager[argv[1]](*argv[2:]) 39 | else: 40 | print "Error: unknown command '%s'" % argv[1] 41 | 42 | 43 | def shell(): 44 | from IPython import embed 45 | 46 | # add application handlers 47 | for appname in settings.installed_apps: 48 | try: 49 | __import__('%s.models' % appname, fromlist=['%s.models'%appname]) 50 | except ImportError: 51 | pass 52 | 53 | embed() 54 | 55 | def run(project_root): 56 | serverurls = [] 57 | servermodels = [] 58 | 59 | # add Airy static file handler 60 | serverurls.extend([ 61 | (r"/airy/form/", web.FormProcessor), 62 | (r"/airy/(.*)", tornado.web.StaticFileHandler, {"path": os.path.join(AIRY_ROOT, 'static')}) 63 | ]) 64 | 65 | # add Airy core handler 66 | core_router = TornadioRouter(web.AiryCoreHandler, settings.__dict__) 67 | serverurls.extend(core_router.urls) 68 | 69 | # add application handlers 70 | for appname in settings.installed_apps: 71 | # add app urls 72 | appurls = __import__('%s.urls' % appname, fromlist=['%s.url'%appname]) 73 | urlpatterns = getattr(appurls, 'urlpatterns') 74 | serverurls.extend(urlpatterns) 75 | try: 76 | models = __import__('%s.models' % appname, fromlist=['%s.models'%appname]) 77 | servermodels.append(models) 78 | except ImportError: 79 | pass 80 | 81 | # restart on code change 82 | for root,dirs,files in os.walk(settings.template_path): 83 | for x in files: 84 | if os.path.splitext(x)[1].lower() == '.html': # track templates 85 | autoreload._watched_files.add(os.path.join(root,x)) 86 | 87 | application = tornado.web.Application(serverurls, **settings.__dict__) 88 | settings.application = web.site.application = application 89 | web.site.loader = template.Loader(settings.template_path) 90 | 91 | SocketServer(application) 92 | 93 | -------------------------------------------------------------------------------- /airy/core/monitor.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import signal 5 | import threading 6 | import atexit 7 | import Queue 8 | 9 | _interval = 1.0 10 | _times = {} 11 | _files = [] 12 | 13 | _running = False 14 | _queue = Queue.Queue() 15 | _lock = threading.Lock() 16 | 17 | def _restart_parent(*args, **kwargs): 18 | sys.exit(3) 19 | 20 | signal.signal(signal.SIGINT, _restart_parent) 21 | 22 | def _restart(path): 23 | _queue.put(True) 24 | prefix = 'monitor (pid=%d):' % os.getpid() 25 | #print >> sys.stderr, '%s Change detected to \'%s\'.' % (prefix, path) 26 | #print >> sys.stderr, '%s Triggering process restart.' % prefix 27 | os.kill(os.getpid(), signal.SIGINT) 28 | 29 | def _modified(path): 30 | try: 31 | # If path doesn't denote a file and were previously 32 | # tracking it, then it has been removed or the file type 33 | # has changed so force a restart. If not previously 34 | # tracking the file then we can ignore it as probably 35 | # pseudo reference such as when file extracted from a 36 | # collection of modules contained in a zip file. 37 | 38 | if not os.path.isfile(path): 39 | return path in _times 40 | 41 | # Check for when file last modified. 42 | 43 | mtime = os.stat(path).st_mtime 44 | if path not in _times: 45 | _times[path] = mtime 46 | 47 | # Force restart when modification time has changed, even 48 | # if time now older, as that could indicate older file 49 | # has been restored. 50 | 51 | if mtime != _times[path]: 52 | return True 53 | except: 54 | # If any exception occured, likely that file has been 55 | # been removed just before stat(), so force a restart. 56 | 57 | return True 58 | 59 | return False 60 | 61 | def _monitor(): 62 | while 1: 63 | # Check modification times on all files in sys.modules. 64 | 65 | for module in sys.modules.values(): 66 | if not hasattr(module, '__file__'): 67 | continue 68 | path = getattr(module, '__file__') 69 | if not path: 70 | continue 71 | if os.path.splitext(path)[1] in ['.pyc', '.pyo', '.pyd']: 72 | path = path[:-1] 73 | if _modified(path): 74 | return _restart(path) 75 | 76 | # Check modification times on files which have 77 | # specifically been registered for monitoring. 78 | 79 | for path in _files: 80 | if _modified(path): 81 | return _restart(path) 82 | 83 | # Go to sleep for specified interval. 84 | 85 | try: 86 | return _queue.get(timeout=_interval) 87 | except: 88 | pass 89 | 90 | _thread = threading.Thread(target=_monitor) 91 | _thread.setDaemon(True) 92 | 93 | def _exiting(): 94 | try: 95 | _queue.put(True) 96 | except: 97 | pass 98 | try: 99 | _thread.join() 100 | except RuntimeError: 101 | pass 102 | 103 | atexit.register(_exiting) 104 | 105 | def track(path): 106 | if not path in _files: 107 | _files.append(path) 108 | 109 | def start(interval=1.0): 110 | global _interval 111 | if interval < _interval: 112 | _interval = interval 113 | 114 | global _running 115 | _lock.acquire() 116 | if not _running: 117 | prefix = 'monitor (pid=%d):' % os.getpid() 118 | # print >> sys.stderr, '%s Starting change monitor.' % prefix 119 | _running = True 120 | _thread.start() 121 | _lock.release() 122 | -------------------------------------------------------------------------------- /airy/core/reportbug.py: -------------------------------------------------------------------------------- 1 | from airy.core.conf import settings 2 | from airy.core.mail import mail_admins 3 | 4 | def report_on_fail(function): 5 | 6 | def wrapped(ins, *args, **kwargs): 7 | try: 8 | return function(ins, *args, **kwargs) 9 | except: 10 | if settings.debug: 11 | raise 12 | ReportBug(ins, args=args, kwargs=kwargs) 13 | 14 | wrapped.__doc__ = function.__doc__ 15 | wrapped.__name__ = function.__name__ 16 | 17 | return wrapped 18 | 19 | 20 | def ReportBug(handler=None, args=[], kwargs={}): 21 | 22 | import sys 23 | import traceback 24 | import os 25 | 26 | # Mail the admins with the error 27 | exc_info = sys.exc_info() 28 | 29 | message = u'\n\nargs:\n %s\n\nkwargs:\n%s\n\n' % (args, kwargs) 30 | message += u'%s\n\n' % ('\n'.join(traceback.format_exception(*exc_info)),) 31 | 32 | exp = sys.exc_info() 33 | 34 | subject = str(exp[1]).strip() 35 | text_content = message 36 | mail_admins(subject, text_content) 37 | 38 | -------------------------------------------------------------------------------- /airy/core/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/letolab/airy/4dc53bec4aa0cd9b983a0a626fecdf49d14bdf94/airy/core/serializers/__init__.py -------------------------------------------------------------------------------- /airy/core/serializers/base.py: -------------------------------------------------------------------------------- 1 | from airy.core.db import * 2 | from mongoengine.queryset import QuerySet 3 | 4 | class BaseSerializer(object): 5 | current_level = 0 6 | levels = 0 7 | 8 | fields = () 9 | exclude = () 10 | 11 | def __init__(self, levels=1, fields=(), exclude=(), *args, **kwargs): 12 | self.levels = levels 13 | self.fields = fields 14 | self.exclude = exclude 15 | 16 | def to_python(self, obj): 17 | if isinstance(obj, QuerySet): 18 | return [self.to_python(item) for item in obj] 19 | 20 | if not obj: 21 | return obj 22 | 23 | if issubclass(obj.__class__, Document): 24 | 25 | return_data = [] 26 | 27 | self.current_level += 1 28 | 29 | if self.levels and self.current_level > self.levels: 30 | return_data.append(('id', str(obj.id))) 31 | return_data.append(('__str__', unicode(obj))) 32 | 33 | else: 34 | for field_name in obj._fields: 35 | 36 | if field_name in self.exclude: 37 | continue 38 | 39 | if self.fields and not field_name in self.fields: 40 | continue 41 | 42 | data = getattr(obj, field_name, '') 43 | field_type = obj._fields[field_name] 44 | 45 | if isinstance(field_type, StringField): 46 | return_data.append((field_name, unicode(data))) 47 | elif isinstance(field_type, FloatField): 48 | return_data.append((field_name, float(data))) 49 | elif isinstance(field_type, IntField): 50 | return_data.append((field_name, int(data))) 51 | elif isinstance(field_type, ListField): 52 | return_data.append((field_name, [self.to_python(item) for item in data])) 53 | elif isinstance(field_type, ReferenceField): 54 | return_data.append((field_name, self.to_python(data))) 55 | else: 56 | return_data.append((field_name, unicode(data))) 57 | # You can define your logic for returning elements 58 | 59 | self.current_level -= 1 60 | 61 | return dict(return_data) 62 | 63 | return {} 64 | 65 | def serialize(self, queryset): 66 | raise NotImplementedError 67 | -------------------------------------------------------------------------------- /airy/core/serializers/json.py: -------------------------------------------------------------------------------- 1 | from airy.utils import simplejson 2 | from airy.core.serializers.base import BaseSerializer 3 | 4 | class JSONSerializer(BaseSerializer): 5 | 6 | def serialize(self, queryset): 7 | return simplejson.dumps(super(JSONSerializer, self).to_python(queryset)) 8 | -------------------------------------------------------------------------------- /airy/core/ve.py: -------------------------------------------------------------------------------- 1 | "Virtualenv utils" 2 | import sys 3 | import os 4 | import subprocess 5 | import virtualenv 6 | import shutil 7 | from os import path 8 | 9 | def check_ve(project_root, argv): 10 | VE_ROOT = path.join(project_root, '.ve') 11 | VE_TIMESTAMP = path.join(VE_ROOT, 'timestamp') 12 | REQUIREMENTS = path.join(project_root, 'requirements.pip') 13 | 14 | envtime = path.exists(VE_ROOT) and path.getmtime(VE_ROOT) or 0 15 | envreqs = path.exists(VE_TIMESTAMP) and path.getmtime(VE_TIMESTAMP) or 0 16 | envspec = path.getmtime(REQUIREMENTS) 17 | 18 | def go_to_ve(ve_root): 19 | # going into ve 20 | if not ve_root in sys.prefix: 21 | retcode = 3 22 | while retcode == 3: 23 | env = os.environ 24 | if sys.platform == 'win32': 25 | python = path.join(VE_ROOT, 'Scripts', 'python.exe') 26 | elif sys.platform == 'darwin': 27 | # temporary fix for broken virtualenv in macports 28 | import airy 29 | env["PYTHONPATH"] = "%s:" % path.join(path.abspath(path.dirname(airy.__file__)), '..') + env.get('PYTHONPATH', '') 30 | python = path.join(VE_ROOT, 'bin', 'python') 31 | else: 32 | python = path.join(VE_ROOT, 'bin', 'python') 33 | try: 34 | retcode = subprocess.call([python, path.join(project_root, 'manage.py')] + argv[1:]) 35 | except KeyboardInterrupt: 36 | retcode = 1 37 | sys.exit(retcode) 38 | 39 | update_ve = 'update_ve' in argv 40 | if update_ve or envtime < envspec or envreqs < envspec: 41 | if update_ve: 42 | # install ve 43 | if envtime < envspec: 44 | if path.exists(VE_ROOT): 45 | shutil.rmtree(VE_ROOT) 46 | virtualenv.logger = virtualenv.Logger(consumers=[]) 47 | virtualenv.create_environment(VE_ROOT, site_packages=True) 48 | 49 | go_to_ve(VE_ROOT) 50 | 51 | # check requirements 52 | if update_ve or envreqs < envspec: 53 | import pip 54 | pip.main(initial_args=['install', '-r', REQUIREMENTS, '--upgrade']) 55 | file(VE_TIMESTAMP, 'w').close() 56 | sys.exit(0) 57 | else: 58 | print "VirtualEnv needs to be updated" 59 | print "Run 'python manage.py update_ve'" 60 | sys.exit(1) 61 | 62 | go_to_ve(VE_ROOT) 63 | -------------------------------------------------------------------------------- /airy/forms/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django validation and HTML form handling. 3 | 4 | TODO: 5 | Default value for field 6 | Field labels 7 | Nestable Forms 8 | FatalValidationError -- short-circuits all other validators on a form 9 | ValidationWarning 10 | "This form field requires foo.js" and form.js_includes() 11 | """ 12 | 13 | from airy.core.exceptions import ValidationError 14 | from widgets import * 15 | from fields import * 16 | from forms import * 17 | 18 | -------------------------------------------------------------------------------- /airy/forms/extras/__init__.py: -------------------------------------------------------------------------------- 1 | from widgets import * 2 | -------------------------------------------------------------------------------- /airy/forms/extras/widgets.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extra HTML Widget classes 3 | """ 4 | 5 | import time 6 | import datetime 7 | import re 8 | 9 | from airy.forms.widgets import Widget, Select 10 | from airy.utils import datetime_safe 11 | from airy.utils.dates import MONTHS 12 | from airy.utils.safestring import mark_safe 13 | from airy.utils.formats import get_format 14 | from airy.core.conf import settings 15 | 16 | __all__ = ('SelectDateWidget',) 17 | 18 | RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$') 19 | 20 | def _parse_date_fmt(): 21 | fmt = get_format('DATE_FORMAT') 22 | escaped = False 23 | output = [] 24 | for char in fmt: 25 | if escaped: 26 | escaped = False 27 | elif char == '\\': 28 | escaped = True 29 | elif char in 'Yy': 30 | output.append('year') 31 | #if not self.first_select: self.first_select = 'year' 32 | elif char in 'bEFMmNn': 33 | output.append('month') 34 | #if not self.first_select: self.first_select = 'month' 35 | elif char in 'dj': 36 | output.append('day') 37 | #if not self.first_select: self.first_select = 'day' 38 | return output 39 | 40 | class SelectDateWidget(Widget): 41 | """ 42 | A Widget that splits date input into three 11 | 12 |
  • Create Account
  • 13 |
  • Forgot your password?
  • 14 | 15 | 16 | 17 | 18 | 19 | {% end %} 20 | -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/page.html: -------------------------------------------------------------------------------- 1 | {% extends "../page.html" %} 2 | 3 | {% block title %}{% block extra_title %}{% end %}Your Profile{% end %} 4 | 5 | {% block menu_profile %}class="active"{% end %} 6 | -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/password_recovery/new_password.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | {% raw form.as_p() %} 5 | 6 |
    7 |
    8 |
    -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/password_recovery/password_reset_sent.html: -------------------------------------------------------------------------------- 1 | Please, check your email! -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/password_recovery/recovery.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | {% raw form.as_p() %} 5 | 6 |
    7 |
    8 |
    -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/bio.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/contact.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/first_name.html: -------------------------------------------------------------------------------- 1 | {{ item }} -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/foreign_profile.html: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | 3 |
    4 | 5 |
    6 | 7 |
    8 |
    9 | {% include 'picture.html' %} 10 |
    11 |

    12 |
    13 | 14 |
    15 | 16 |
    17 |

    18 | {{ user.first_name }} {{ user.last_name }} 19 |

    20 | 21 |

    {{ user.headline }}

    22 | 23 |

    Summary  

    24 |

    {% if user.bio %}{{ user.bio }}{% else %}---{% end %}

    25 |
    26 | 27 |

    Contact  

    28 |

    {% if user.contact %}{{ user.contact }}{% else %}---{% end %}

    29 |
    30 | 31 |

    I am on other sites  

    32 |

    {% if user.other_sites %}{{ user.other_sites }}{% else %}---{% end %}

    33 | 34 |
    35 | 36 |
    37 | 38 | {% end %} -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/headline.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/last_name.html: -------------------------------------------------------------------------------- 1 | {{ item }} -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/main.html: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | 3 | {% set current_user = user %} 4 | 5 |
    6 |
    7 |
    8 |
    9 | {% include 'picture.html' %} 10 |
    11 |
    12 | Change picture 13 |
    14 | 15 |

    16 |

    Preview:

    17 | See your profile how others see it. 18 |
    19 |
    20 |
    21 | 22 |
    23 |

    24 | {{ user.first_name }} 25 | {{ user.last_name }} 26 |

    27 | 28 |

    {% if user.headline %}{{ user.headline }}{% else %}Add headline{% end %}

    29 | 30 |

    Summary  

    31 |

    {% if user.bio %}{{ user.bio }}{% else %}---{% end %}

    32 |
    33 | 34 |

    Contact  

    35 |

    {% if user.contact %}{{ user.contact }}{% else %}---{% end %}

    36 |
    37 | 38 |

    I am on other sites  

    39 |

    {% if user.other_sites %}{{ user.other_sites }}{% else %}---{% end %}

    40 | 41 |
    42 | 43 |
    44 | 45 | 46 | {% end %} -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/other_sites.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/picture.html: -------------------------------------------------------------------------------- 1 | {% if user.picture_url %} 2 | {{ user.username }}'s Picture' 3 | {% else %} 4 | {{ user.username }}'s Picture' 5 | {% end %} -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/profile/picture_upload.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Drag and Drop
    a file here

    3 |
    or
    4 |
    5 |
    6 | {% raw form.as_p() %} 7 | 8 | Cancel 9 |
    -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/register.html: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | 3 |
    4 |
    5 |

    Create an Account

    6 |
    7 |
      8 | {% raw form.as_p() %} 9 |
    • 10 | 11 |
    • 12 |
    13 |
    14 |
    15 |
    16 | {% end %} 17 | -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/user_in_menu.html: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /airy/skeleton/project/templates/users/user_out_menu.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /airy/skeleton/project/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/letolab/airy/4dc53bec4aa0cd9b983a0a626fecdf49d14bdf94/airy/skeleton/project/users/__init__.py -------------------------------------------------------------------------------- /airy/skeleton/project/users/auth.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from hashlib import md5 3 | 4 | from users.models import User, Session 5 | from mongoengine.queryset import DoesNotExist 6 | 7 | 8 | def login(user, raw_password, handler): 9 | session_key = md5(user.username + raw_password) 10 | handler.set_secure_cookie("session_key", session_key.hexdigest()) 11 | 12 | session = Session.objects.get_or_create(user=user)[0] 13 | session.session_key = session_key.hexdigest() 14 | session.save() 15 | 16 | user.last_login = datetime.now() 17 | user.save() 18 | 19 | 20 | def authenticate(password, username=None, email=None): 21 | try: 22 | if not email: 23 | user = User.objects.get(username=username) 24 | else: 25 | user = User.objects.get(email=email) 26 | if not user.check_password(password): 27 | return None 28 | return user 29 | except DoesNotExist: 30 | return None 31 | 32 | 33 | def get_current_user(handler, *args, **kwargs): 34 | session_key = handler.get_secure_cookie("session_key") 35 | 36 | if not session_key: 37 | return None 38 | try: 39 | session = Session.objects.get(session_key=session_key) 40 | return session.user 41 | except DoesNotExist: 42 | return None 43 | return None 44 | -------------------------------------------------------------------------------- /airy/skeleton/project/users/context_processors.py: -------------------------------------------------------------------------------- 1 | from airy.core.conf import settings as asettings 2 | 3 | def settings(handler, **kwargs): 4 | return {'settings': asettings} 5 | 6 | def user(handler, **kwargs): 7 | return {'user': handler.get_current_user()} 8 | -------------------------------------------------------------------------------- /airy/skeleton/project/users/decorators.py: -------------------------------------------------------------------------------- 1 | from airy.core.conf import settings 2 | from mongoengine.queryset import DoesNotExist 3 | from users.auth import get_current_user 4 | 5 | def login_required(function): 6 | 7 | def wrapped(handler, *args, **kwargs): 8 | if get_current_user(handler): 9 | return function(handler, *args, **kwargs) 10 | else: 11 | handler.redirect(getattr(settings, 'login_url', '/accounts/login')) 12 | 13 | wrapped.__doc__ = function.__doc__ 14 | wrapped.__name__ = function.__name__ 15 | 16 | return wrapped 17 | -------------------------------------------------------------------------------- /airy/skeleton/project/users/forms.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from md5 import md5 3 | from airy import forms 4 | from airy.core.conf import settings 5 | from os.path import join 6 | 7 | from users.auth import authenticate, login 8 | from users.models import User, Session 9 | 10 | 11 | class RegistrationForm(forms.Form): 12 | email = forms.EmailField() 13 | first_name = forms.CharField() 14 | last_name = forms.CharField() 15 | password = forms.CharField(widget=forms.PasswordInput(render_value=True)) 16 | password2 = forms.CharField(widget=forms.PasswordInput(render_value=True), label='Confirm password') 17 | 18 | def clean_email(self): 19 | if User.objects.filter(email=self.cleaned_data['email']).count() > 0: 20 | raise forms.ValidationError('A user with such email is already registered') 21 | return self.cleaned_data['email'] 22 | 23 | def clean(self, *args, **kwargs): 24 | super(RegistrationForm, self).clean(*args, **kwargs) 25 | if 'password' in self.cleaned_data and 'password2' in self.cleaned_data: 26 | if self.cleaned_data['password'] != self.cleaned_data['password2']: 27 | self._errors['password'] = [u'Passwords must match.'] 28 | self._errors['password2'] = [u'Passwords must match'] 29 | return self.cleaned_data 30 | 31 | def save(self, obj): 32 | user = User( 33 | username=self.cleaned_data['email'], 34 | first_name=self.cleaned_data['first_name'], 35 | last_name=self.cleaned_data['last_name'], 36 | email=self.cleaned_data['email'], 37 | education=[], 38 | experience=[], 39 | services=[] 40 | ) 41 | user.set_password(self.cleaned_data['password']) 42 | user.save() 43 | 44 | user = authenticate(username=self.cleaned_data['email'], password=self.cleaned_data['password']) 45 | 46 | session_key = md5(self.cleaned_data['username'] + self.cleaned_data['password']) 47 | obj.set_secure_cookie("session_key", session_key.hexdigest()) 48 | session = Session.objects.get_or_create(user = user)[0] 49 | session.session_key = session_key.hexdigest() 50 | session.save() 51 | 52 | return user 53 | 54 | 55 | class LoginForm(forms.Form): 56 | username = forms.CharField(label="E-mail") 57 | password = forms.CharField(widget=forms.PasswordInput(render_value=True)) 58 | 59 | def clean(self): 60 | if 'username' in self.cleaned_data and 'password' in self.cleaned_data: 61 | username = self.cleaned_data['username'] 62 | if not '@' in username: 63 | user = authenticate(username=username, password=self.cleaned_data['password']) 64 | else: 65 | user = authenticate(email=username, password=self.cleaned_data['password']) 66 | if not user: 67 | raise forms.ValidationError('Email or password are incorrect') 68 | else: 69 | return None 70 | 71 | self.user = user 72 | return user 73 | 74 | def save(self, handler): 75 | login(self.user, self.cleaned_data['password'], handler) 76 | return self.user 77 | 78 | 79 | class FileUploadForm(forms.Form): 80 | picture = forms.FileField(label='Choose a file') 81 | 82 | def save(self, user=None, *args, **kwargs): 83 | file = open(join(settings.static_path, 'media', self.cleaned_data['picture'].name), 'wb') 84 | file.write(self.cleaned_data['picture'].read()) 85 | file.close() 86 | if user: 87 | user.picture_url = '/static/media/%s' % self.cleaned_data['picture'].name 88 | user.save() 89 | return '/static/media/%s' % self.cleaned_data['picture'].name 90 | 91 | 92 | class PasswordRecoveryForm(forms.Form): 93 | email_or_login = forms.CharField( 94 | widget=forms.TextInput(attrs={'class': 'large input-text'}), 95 | label='Please enter your email or password' 96 | ) 97 | 98 | def clean_email_or_login(self): 99 | data = self.cleaned_data['email_or_login'] 100 | try: 101 | if '@' in data: 102 | self.user = User.objects.get(email=data) 103 | else: 104 | self.user = User.objects.get(username=data) 105 | except Exception as e: 106 | raise forms.ValidationError('A user with such email or username does not exist') 107 | return self.cleaned_data['email_or_login'] 108 | 109 | def save(self): 110 | return self.user 111 | 112 | 113 | class NewPasswordForm(forms.Form): 114 | password = forms.CharField( 115 | widget=forms.PasswordInput(render_value=True), 116 | label='Please enter new password' 117 | ) 118 | password2 = forms.CharField( 119 | widget=forms.PasswordInput(render_value=True), 120 | label='Confirm password' 121 | ) 122 | 123 | def clean(self, *args, **kwargs): 124 | super(NewPasswordForm, self).clean(*args, **kwargs) 125 | if 'password' in self.cleaned_data and 'password2' in self.cleaned_data: 126 | if self.cleaned_data['password'] != self.cleaned_data['password2']: 127 | self._errors['password'] = [u'Passwords must match.'] 128 | self._errors['password2'] = [u'Passwords must match'] 129 | return self.cleaned_data 130 | 131 | def save(self, handler, user): 132 | user.set_password(self.cleaned_data['password']) 133 | user.save() 134 | login(user, self.cleaned_data['password'], handler) 135 | return user -------------------------------------------------------------------------------- /airy/skeleton/project/users/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from hashlib import md5 3 | 4 | from airy.core.db import * 5 | 6 | 7 | class PasswordResetToken(Document): 8 | expired = DateTimeField() 9 | token = StringField(max_length=128) 10 | 11 | 12 | class User(Document): 13 | username = StringField(max_length=30) 14 | password = StringField(max_length=128) 15 | first_name = StringField(max_length=30) 16 | last_name = StringField(max_length=30) 17 | email = StringField(max_length=30) 18 | last_login = DateTimeField(default=datetime.now) 19 | date_joined = DateTimeField(default=datetime.now) 20 | picture_url = StringField(max_length=512, default='') 21 | picture_height = IntField(default=0) 22 | picture_width = IntField(default=0) 23 | headline = StringField(max_length=512, default='') 24 | country = StringField(max_length=512, required=False) 25 | state = StringField(max_length=512, required=False) 26 | city = StringField(max_length=512, required=False) 27 | bio = StringField(default='') 28 | contact = StringField(default='') 29 | other_sites = StringField(default='') 30 | interests = ListField(ReferenceField("Tag")) 31 | recommended_books = ListField(ReferenceField("Book")) 32 | password_reset_token_list = ListField(ReferenceField(PasswordResetToken)) 33 | rating = IntField(default=0) 34 | 35 | def __unicode__(self): 36 | if self.first_name and self.last_name: 37 | return u'%s %s' % (self.first_name, self.last_name) 38 | return u'%s' % self.username 39 | 40 | def get_picture_url(self): 41 | if self.picture: 42 | return self.picture.url 43 | return getattr(settings, 'DEFAULT_PICTURE_URL', 44 | '/static/images/profile-default.png') 45 | 46 | def check_password(self, raw_password): 47 | if self.password == md5(raw_password).hexdigest(): 48 | return True 49 | return False 50 | 51 | def set_password(self, raw_password): 52 | self.password = md5(raw_password).hexdigest() 53 | 54 | 55 | class Session(Document): 56 | user = ReferenceField(User) 57 | session_key = StringField(max_length=64) 58 | 59 | 60 | class Tag(Document): 61 | tag = StringField(max_length=64) 62 | 63 | 64 | class Interest(Tag): 65 | pass 66 | 67 | 68 | class Book(Document): 69 | text = StringField(max_length=1024) 70 | ref = URLField(required=False) 71 | -------------------------------------------------------------------------------- /airy/skeleton/project/users/urls.py: -------------------------------------------------------------------------------- 1 | from handlers import * 2 | 3 | urlpatterns = [ 4 | (r"/.*", IndexHandler), # root handler to accept old-style HTTP requests 5 | 6 | (r"/", HomeHandler), 7 | (r"/accounts/login", AccountsLoginHandler), 8 | (r"/accounts/logout", AccountsLogoutHandler), 9 | (r"/accounts/profile", AccountsProfileHandler), 10 | (r"/accounts/profile/change/", AccountsChangeUserInfoHandler), 11 | (r"/accounts/profile/delete/", AccountsProfileDeleteHandler), 12 | (r"/accounts/profile/(?P[^\/]+)", AccountsForeignProfileHandler), 13 | (r"/accounts/profile/picture/(?P\w+)", FileUpload), 14 | (r"/accounts/recovery/(?P.+)?", AccountsRecoverPasswordHandler), 15 | (r"/accounts/register", AccountsRegisterHandler), 16 | 17 | # Username handler 18 | (r"/u/(?P\w+)", AccountsForeignProfileHandler), 19 | ] 20 | 21 | 22 | -------------------------------------------------------------------------------- /airy/static/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/letolab/airy/4dc53bec4aa0cd9b983a0a626fecdf49d14bdf94/airy/static/WebSocketMain.swf -------------------------------------------------------------------------------- /airy/static/lib/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Cookie plugin 3 | * 4 | * Copyright (c) 2010 Klaus Hartl (stilbuero.de) 5 | * Dual licensed under the MIT and GPL licenses: 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * http://www.gnu.org/licenses/gpl.html 8 | * 9 | * Modified by Alex Berezovskiy 10 | */ 11 | 12 | $(function() { 13 | 14 | jQuery.cookie = function (key, value, options) { 15 | 16 | // key and at least value given, set cookie... 17 | if (arguments.length > 1 && String(value) !== "[object Object]") { 18 | options = jQuery.extend({}, options); 19 | 20 | if (value === null || value === undefined) { 21 | options.expires = -1; 22 | } 23 | 24 | if (typeof options.expires === 'number') { 25 | var days = options.expires, t = options.expires = new Date(); 26 | t.setDate(t.getDate() + days); 27 | } 28 | 29 | value = String(value); 30 | 31 | return (document.cookie = [ 32 | encodeURIComponent(key), '=', 33 | options.raw ? value : encodeURIComponent(value), 34 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 35 | options.path ? '; path=' + options.path : '', 36 | options.domain ? '; domain=' + options.domain : '', 37 | options.secure ? '; secure' : '' 38 | ].join('')); 39 | } 40 | 41 | // key and possibly options given, get cookie... 42 | options = value || {}; 43 | var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent; 44 | return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null; 45 | }; 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /airy/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/letolab/airy/4dc53bec4aa0cd9b983a0a626fecdf49d14bdf94/airy/utils/__init__.py -------------------------------------------------------------------------------- /airy/utils/_os.py: -------------------------------------------------------------------------------- 1 | import os 2 | import stat 3 | from os.path import join, normcase, normpath, abspath, isabs, sep 4 | from airy.utils.encoding import force_unicode 5 | 6 | # Define our own abspath function that can handle joining 7 | # unicode paths to a current working directory that has non-ASCII 8 | # characters in it. This isn't necessary on Windows since the 9 | # Windows version of abspath handles this correctly. The Windows 10 | # abspath also handles drive letters differently than the pure 11 | # Python implementation, so it's best not to replace it. 12 | if os.name == 'nt': 13 | abspathu = abspath 14 | else: 15 | def abspathu(path): 16 | """ 17 | Version of os.path.abspath that uses the unicode representation 18 | of the current working directory, thus avoiding a UnicodeDecodeError 19 | in join when the cwd has non-ASCII characters. 20 | """ 21 | if not isabs(path): 22 | path = join(os.getcwdu(), path) 23 | return normpath(path) 24 | 25 | def safe_join(base, *paths): 26 | """ 27 | Joins one or more path components to the base path component intelligently. 28 | Returns a normalized, absolute version of the final path. 29 | 30 | The final path must be located inside of the base path component (otherwise 31 | a ValueError is raised). 32 | """ 33 | # We need to use normcase to ensure we don't false-negative on case 34 | # insensitive operating systems (like Windows). 35 | base = force_unicode(base) 36 | paths = [force_unicode(p) for p in paths] 37 | final_path = normcase(abspathu(join(base, *paths))) 38 | base_path = normcase(abspathu(base)) 39 | base_path_len = len(base_path) 40 | # Ensure final_path starts with base_path and that the next character after 41 | # the final path is os.sep (or nothing, in which case final_path must be 42 | # equal to base_path). 43 | if not final_path.startswith(base_path) \ 44 | or final_path[base_path_len:base_path_len+1] not in ('', sep): 45 | raise ValueError('The joined path (%s) is located outside of the base ' 46 | 'path component (%s)' % (final_path, base_path)) 47 | return final_path 48 | 49 | def rmtree_errorhandler(func, path, exc_info): 50 | """ 51 | On Windows, some files are read-only (e.g. in in .svn dirs), so when 52 | rmtree() tries to remove them, an exception is thrown. 53 | We catch that here, remove the read-only attribute, and hopefully 54 | continue without problems. 55 | """ 56 | exctype, value = exc_info[:2] 57 | # lookin for a windows error 58 | if exctype is not WindowsError or 'Access is denied' not in str(value): 59 | raise 60 | # file type should currently be read only 61 | if ((os.stat(path).st_mode & stat.S_IREAD) != stat.S_IREAD): 62 | raise 63 | # convert to read/write 64 | os.chmod(path, stat.S_IWRITE) 65 | # use the original function to repeat the operation 66 | func(path) 67 | 68 | -------------------------------------------------------------------------------- /airy/utils/autoreload.py: -------------------------------------------------------------------------------- 1 | # Autoreloading launcher. 2 | # Borrowed from Peter Hunt and the CherryPy project (http://www.cherrypy.org). 3 | # Some taken from Ian Bicking's Paste (http://pythonpaste.org/). 4 | # 5 | # Portions copyright (c) 2004, CherryPy Team (team@cherrypy.org) 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without modification, 9 | # are permitted provided that the following conditions are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright notice, 12 | # this list of conditions and the following disclaimer. 13 | # * Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # * Neither the name of the CherryPy Team nor the names of its contributors 17 | # may be used to endorse or promote products derived from this software 18 | # without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 24 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | import os, sys, time, signal 32 | 33 | try: 34 | import thread 35 | except ImportError: 36 | import dummy_thread as thread 37 | 38 | # This import does nothing, but it's necessary to avoid some race conditions 39 | # in the threading module. See http://code.djangoproject.com/ticket/2330 . 40 | try: 41 | import threading 42 | except ImportError: 43 | pass 44 | 45 | try: 46 | import termios 47 | except ImportError: 48 | termios = None 49 | 50 | RUN_RELOADER = True 51 | 52 | _mtimes = {} 53 | _win = (sys.platform == "win32") 54 | 55 | def code_changed(): 56 | global _mtimes, _win 57 | for filename in filter(lambda v: v, map(lambda m: getattr(m, "__file__", None), sys.modules.values())): 58 | if filename.endswith(".pyc") or filename.endswith(".pyo"): 59 | filename = filename[:-1] 60 | if not os.path.exists(filename): 61 | continue # File might be in an egg, so it can't be reloaded. 62 | stat = os.stat(filename) 63 | mtime = stat.st_mtime 64 | if _win: 65 | mtime -= stat.st_ctime 66 | if filename not in _mtimes: 67 | _mtimes[filename] = mtime 68 | continue 69 | if mtime != _mtimes[filename]: 70 | _mtimes = {} 71 | return True 72 | return False 73 | 74 | def ensure_echo_on(): 75 | if termios: 76 | fd = sys.stdin 77 | if fd.isatty(): 78 | attr_list = termios.tcgetattr(fd) 79 | if not attr_list[3] & termios.ECHO: 80 | attr_list[3] |= termios.ECHO 81 | if hasattr(signal, 'SIGTTOU'): 82 | old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN) 83 | else: 84 | old_handler = None 85 | termios.tcsetattr(fd, termios.TCSANOW, attr_list) 86 | if old_handler is not None: 87 | signal.signal(signal.SIGTTOU, old_handler) 88 | 89 | def reloader_thread(): 90 | ensure_echo_on() 91 | while RUN_RELOADER: 92 | if code_changed(): 93 | sys.exit(3) # force reload 94 | time.sleep(1) 95 | 96 | def restart_with_reloader(): 97 | while True: 98 | args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions] + sys.argv 99 | if sys.platform == "win32": 100 | args = ['"%s"' % arg for arg in args] 101 | new_environ = os.environ.copy() 102 | new_environ["RUN_MAIN"] = 'true' 103 | exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ) 104 | if exit_code != 3: 105 | return exit_code 106 | 107 | def python_reloader(main_func, args, kwargs): 108 | if os.environ.get("RUN_MAIN") == "true": 109 | thread.start_new_thread(main_func, args, kwargs) 110 | try: 111 | reloader_thread() 112 | except KeyboardInterrupt: 113 | pass 114 | else: 115 | try: 116 | sys.exit(restart_with_reloader()) 117 | except KeyboardInterrupt: 118 | pass 119 | 120 | def jython_reloader(main_func, args, kwargs): 121 | from _systemrestart import SystemRestart 122 | thread.start_new_thread(main_func, args) 123 | while True: 124 | if code_changed(): 125 | raise SystemRestart 126 | time.sleep(1) 127 | 128 | 129 | def main(main_func, args=None, kwargs=None): 130 | if args is None: 131 | args = () 132 | if kwargs is None: 133 | kwargs = {} 134 | if sys.platform.startswith('java'): 135 | reloader = jython_reloader 136 | else: 137 | reloader = python_reloader 138 | reloader(main_func, args, kwargs) 139 | 140 | -------------------------------------------------------------------------------- /airy/utils/checksums.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common checksum routines (used in multiple localflavor/ cases, for example). 3 | """ 4 | 5 | __all__ = ['luhn',] 6 | 7 | LUHN_ODD_LOOKUP = (0, 2, 4, 6, 8, 1, 3, 5, 7, 9) # sum_of_digits(index * 2) 8 | 9 | def luhn(candidate): 10 | """ 11 | Checks a candidate number for validity according to the Luhn 12 | algorithm (used in validation of, for example, credit cards). 13 | Both numeric and string candidates are accepted. 14 | """ 15 | if not isinstance(candidate, basestring): 16 | candidate = str(candidate) 17 | try: 18 | evens = sum([int(c) for c in candidate[-1::-2]]) 19 | odds = sum([LUHN_ODD_LOOKUP[int(c)] for c in candidate[-2::-2]]) 20 | return ((evens + odds) % 10 == 0) 21 | except ValueError: # Raised if an int conversion fails 22 | return False 23 | -------------------------------------------------------------------------------- /airy/utils/copycompat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Fixes Python 2.4's failure to deepcopy unbound functions. 3 | """ 4 | 5 | import copy 6 | import types 7 | 8 | # Monkeypatch copy's deepcopy registry to handle functions correctly. 9 | if (hasattr(copy, '_deepcopy_dispatch') and types.FunctionType not in copy._deepcopy_dispatch): 10 | copy._deepcopy_dispatch[types.FunctionType] = copy._deepcopy_atomic 11 | 12 | # Pose as the copy module now. 13 | del copy, types 14 | from copy import * 15 | -------------------------------------------------------------------------------- /airy/utils/crypto.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django's standard crypto functions and utilities. 3 | """ 4 | import hmac 5 | 6 | from airy.core.conf import settings 7 | from airy.utils.hashcompat import sha_constructor, sha_hmac 8 | 9 | 10 | def salted_hmac(key_salt, value, secret=None): 11 | """ 12 | Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a 13 | secret (which defaults to settings.SECRET_KEY). 14 | 15 | A different key_salt should be passed in for every application of HMAC. 16 | """ 17 | if secret is None: 18 | secret = settings.SECRET_KEY 19 | 20 | # We need to generate a derived key from our base key. We can do this by 21 | # passing the key_salt and our base key through a pseudo-random function and 22 | # SHA1 works nicely. 23 | 24 | key = sha_constructor(key_salt + secret).digest() 25 | 26 | # If len(key_salt + secret) > sha_constructor().block_size, the above 27 | # line is redundant and could be replaced by key = key_salt + secret, since 28 | # the hmac module does the same thing for keys longer than the block size. 29 | # However, we need to ensure that we *always* do this. 30 | 31 | return hmac.new(key, msg=value, digestmod=sha_hmac) 32 | 33 | 34 | def constant_time_compare(val1, val2): 35 | """ 36 | Returns True if the two strings are equal, False otherwise. 37 | 38 | The time taken is independent of the number of characters that match. 39 | """ 40 | if len(val1) != len(val2): 41 | return False 42 | result = 0 43 | for x, y in zip(val1, val2): 44 | result |= ord(x) ^ ord(y) 45 | return result == 0 46 | -------------------------------------------------------------------------------- /airy/utils/daemonize.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | if os.name == 'posix': 5 | def become_daemon(our_home_dir='.', out_log='/dev/null', 6 | err_log='/dev/null', umask=022): 7 | "Robustly turn into a UNIX daemon, running in our_home_dir." 8 | # First fork 9 | try: 10 | if os.fork() > 0: 11 | sys.exit(0) # kill off parent 12 | except OSError, e: 13 | sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror)) 14 | sys.exit(1) 15 | os.setsid() 16 | os.chdir(our_home_dir) 17 | os.umask(umask) 18 | 19 | # Second fork 20 | try: 21 | if os.fork() > 0: 22 | os._exit(0) 23 | except OSError, e: 24 | sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror)) 25 | os._exit(1) 26 | 27 | si = open('/dev/null', 'r') 28 | so = open(out_log, 'a+', 0) 29 | se = open(err_log, 'a+', 0) 30 | os.dup2(si.fileno(), sys.stdin.fileno()) 31 | os.dup2(so.fileno(), sys.stdout.fileno()) 32 | os.dup2(se.fileno(), sys.stderr.fileno()) 33 | # Set custom file descriptors so that they get proper buffering. 34 | sys.stdout, sys.stderr = so, se 35 | else: 36 | def become_daemon(our_home_dir='.', out_log=None, err_log=None, umask=022): 37 | """ 38 | If we're not running under a POSIX system, just simulate the daemon 39 | mode by doing redirections and directory changing. 40 | """ 41 | os.chdir(our_home_dir) 42 | os.umask(umask) 43 | sys.stdin.close() 44 | sys.stdout.close() 45 | sys.stderr.close() 46 | if err_log: 47 | sys.stderr = open(err_log, 'a', 0) 48 | else: 49 | sys.stderr = NullDevice() 50 | if out_log: 51 | sys.stdout = open(out_log, 'a', 0) 52 | else: 53 | sys.stdout = NullDevice() 54 | 55 | class NullDevice: 56 | "A writeable object that writes to nowhere -- like /dev/null." 57 | def write(self, s): 58 | pass 59 | -------------------------------------------------------------------------------- /airy/utils/dates.py: -------------------------------------------------------------------------------- 1 | "Commonly-used date structures" 2 | 3 | from airy.utils.translation import ugettext_lazy as _, pgettext_lazy 4 | 5 | WEEKDAYS = { 6 | 0:_('Monday'), 1:_('Tuesday'), 2:_('Wednesday'), 3:_('Thursday'), 4:_('Friday'), 7 | 5:_('Saturday'), 6:_('Sunday') 8 | } 9 | WEEKDAYS_ABBR = { 10 | 0:_('Mon'), 1:_('Tue'), 2:_('Wed'), 3:_('Thu'), 4:_('Fri'), 11 | 5:_('Sat'), 6:_('Sun') 12 | } 13 | WEEKDAYS_REV = { 14 | 'monday':0, 'tuesday':1, 'wednesday':2, 'thursday':3, 'friday':4, 15 | 'saturday':5, 'sunday':6 16 | } 17 | MONTHS = { 18 | 1:_('January'), 2:_('February'), 3:_('March'), 4:_('April'), 5:_('May'), 6:_('June'), 19 | 7:_('July'), 8:_('August'), 9:_('September'), 10:_('October'), 11:_('November'), 20 | 12:_('December') 21 | } 22 | MONTHS_3 = { 23 | 1:_('jan'), 2:_('feb'), 3:_('mar'), 4:_('apr'), 5:_('may'), 6:_('jun'), 24 | 7:_('jul'), 8:_('aug'), 9:_('sep'), 10:_('oct'), 11:_('nov'), 12:_('dec') 25 | } 26 | MONTHS_3_REV = { 27 | 'jan':1, 'feb':2, 'mar':3, 'apr':4, 'may':5, 'jun':6, 'jul':7, 'aug':8, 28 | 'sep':9, 'oct':10, 'nov':11, 'dec':12 29 | } 30 | MONTHS_AP = { # month names in Associated Press style 31 | 1: pgettext_lazy('abbrev. month', 'Jan.'), 32 | 2: pgettext_lazy('abbrev. month', 'Feb.'), 33 | 3: pgettext_lazy('abbrev. month', 'March'), 34 | 4: pgettext_lazy('abbrev. month', 'April'), 35 | 5: pgettext_lazy('abbrev. month', 'May'), 36 | 6: pgettext_lazy('abbrev. month', 'June'), 37 | 7: pgettext_lazy('abbrev. month', 'July'), 38 | 8: pgettext_lazy('abbrev. month', 'Aug.'), 39 | 9: pgettext_lazy('abbrev. month', 'Sept.'), 40 | 10: pgettext_lazy('abbrev. month', 'Oct.'), 41 | 11: pgettext_lazy('abbrev. month', 'Nov.'), 42 | 12: pgettext_lazy('abbrev. month', 'Dec.') 43 | } 44 | MONTHS_ALT = { # required for long date representation by some locales 45 | 1: pgettext_lazy('alt. month', 'January'), 46 | 2: pgettext_lazy('alt. month', 'February'), 47 | 3: pgettext_lazy('alt. month', 'March'), 48 | 4: pgettext_lazy('alt. month', 'April'), 49 | 5: pgettext_lazy('alt. month', 'May'), 50 | 6: pgettext_lazy('alt. month', 'June'), 51 | 7: pgettext_lazy('alt. month', 'July'), 52 | 8: pgettext_lazy('alt. month', 'August'), 53 | 9: pgettext_lazy('alt. month', 'September'), 54 | 10: pgettext_lazy('alt. month', 'October'), 55 | 11: pgettext_lazy('alt. month', 'November'), 56 | 12: pgettext_lazy('alt. month', 'December') 57 | } 58 | -------------------------------------------------------------------------------- /airy/utils/datetime_safe.py: -------------------------------------------------------------------------------- 1 | # Python's datetime strftime doesn't handle dates before 1900. 2 | # These classes override date and datetime to support the formatting of a date 3 | # through its full "proleptic Gregorian" date range. 4 | # 5 | # Based on code submitted to comp.lang.python by Andrew Dalke 6 | # 7 | # >>> datetime_safe.date(1850, 8, 2).strftime("%Y/%m/%d was a %A") 8 | # '1850/08/02 was a Friday' 9 | 10 | from datetime import date as real_date, datetime as real_datetime 11 | import re 12 | import time 13 | 14 | class date(real_date): 15 | def strftime(self, fmt): 16 | return strftime(self, fmt) 17 | 18 | class datetime(real_datetime): 19 | def strftime(self, fmt): 20 | return strftime(self, fmt) 21 | 22 | def combine(self, date, time): 23 | return datetime(date.year, date.month, date.day, time.hour, time.minute, time.microsecond, time.tzinfo) 24 | 25 | def date(self): 26 | return date(self.year, self.month, self.day) 27 | 28 | def new_date(d): 29 | "Generate a safe date from a datetime.date object." 30 | return date(d.year, d.month, d.day) 31 | 32 | def new_datetime(d): 33 | """ 34 | Generate a safe datetime from a datetime.date or datetime.datetime object. 35 | """ 36 | kw = [d.year, d.month, d.day] 37 | if isinstance(d, real_datetime): 38 | kw.extend([d.hour, d.minute, d.second, d.microsecond, d.tzinfo]) 39 | return datetime(*kw) 40 | 41 | # This library does not support strftime's "%s" or "%y" format strings. 42 | # Allowed if there's an even number of "%"s because they are escaped. 43 | _illegal_formatting = re.compile(r"((^|[^%])(%%)*%[sy])") 44 | 45 | def _findall(text, substr): 46 | # Also finds overlaps 47 | sites = [] 48 | i = 0 49 | while 1: 50 | j = text.find(substr, i) 51 | if j == -1: 52 | break 53 | sites.append(j) 54 | i=j+1 55 | return sites 56 | 57 | def strftime(dt, fmt): 58 | if dt.year >= 1900: 59 | return super(type(dt), dt).strftime(fmt) 60 | illegal_formatting = _illegal_formatting.search(fmt) 61 | if illegal_formatting: 62 | raise TypeError("strftime of dates before 1900 does not handle" + illegal_formatting.group(0)) 63 | 64 | year = dt.year 65 | # For every non-leap year century, advance by 66 | # 6 years to get into the 28-year repeat cycle 67 | delta = 2000 - year 68 | off = 6 * (delta // 100 + delta // 400) 69 | year = year + off 70 | 71 | # Move to around the year 2000 72 | year = year + ((2000 - year) // 28) * 28 73 | timetuple = dt.timetuple() 74 | s1 = time.strftime(fmt, (year,) + timetuple[1:]) 75 | sites1 = _findall(s1, str(year)) 76 | 77 | s2 = time.strftime(fmt, (year+28,) + timetuple[1:]) 78 | sites2 = _findall(s2, str(year+28)) 79 | 80 | sites = [] 81 | for site in sites1: 82 | if site in sites2: 83 | sites.append(site) 84 | 85 | s = s1 86 | syear = "%04d" % (dt.year,) 87 | for site in sites: 88 | s = s[:site] + syear + s[site+4:] 89 | return s 90 | -------------------------------------------------------------------------------- /airy/utils/decorators.py: -------------------------------------------------------------------------------- 1 | "Functions that help with dynamically creating decorators for views." 2 | 3 | try: 4 | from functools import wraps, update_wrapper, WRAPPER_ASSIGNMENTS 5 | except ImportError: 6 | from airy.utils.functional import wraps, update_wrapper, WRAPPER_ASSIGNMENTS # Python 2.4 fallback. 7 | 8 | class classonlymethod(classmethod): 9 | def __get__(self, instance, owner): 10 | if instance is not None: 11 | raise AttributeError("This method is available only on the view class.") 12 | return super(classonlymethod, self).__get__(instance, owner) 13 | 14 | def method_decorator(decorator): 15 | """ 16 | Converts a function decorator into a method decorator 17 | """ 18 | # 'func' is a function at the time it is passed to _dec, but will eventually 19 | # be a method of the class it is defined it. 20 | def _dec(func): 21 | def _wrapper(self, *args, **kwargs): 22 | @decorator 23 | def bound_func(*args2, **kwargs2): 24 | return func(self, *args2, **kwargs2) 25 | # bound_func has the signature that 'decorator' expects i.e. no 26 | # 'self' argument, but it is a closure over self so it can call 27 | # 'func' correctly. 28 | return bound_func(*args, **kwargs) 29 | # In case 'decorator' adds attributes to the function it decorates, we 30 | # want to copy those. We don't have access to bound_func in this scope, 31 | # but we can cheat by using it on a dummy function. 32 | @decorator 33 | def dummy(*args, **kwargs): 34 | pass 35 | update_wrapper(_wrapper, dummy) 36 | # Need to preserve any existing attributes of 'func', including the name. 37 | update_wrapper(_wrapper, func) 38 | 39 | return _wrapper 40 | update_wrapper(_dec, decorator) 41 | # Change the name to aid debugging. 42 | _dec.__name__ = 'method_decorator(%s)' % decorator.__name__ 43 | return _dec 44 | 45 | 46 | def decorator_from_middleware_with_args(middleware_class): 47 | """ 48 | Like decorator_from_middleware, but returns a function 49 | that accepts the arguments to be passed to the middleware_class. 50 | Use like:: 51 | 52 | cache_page = decorator_from_middleware_with_args(CacheMiddleware) 53 | # ... 54 | 55 | @cache_page(3600) 56 | def my_view(request): 57 | # ... 58 | """ 59 | return make_middleware_decorator(middleware_class) 60 | 61 | 62 | def decorator_from_middleware(middleware_class): 63 | """ 64 | Given a middleware class (not an instance), returns a view decorator. This 65 | lets you use middleware functionality on a per-view basis. The middleware 66 | is created with no params passed. 67 | """ 68 | return make_middleware_decorator(middleware_class)() 69 | 70 | 71 | def available_attrs(fn): 72 | """ 73 | Return the list of functools-wrappable attributes on a callable. 74 | This is required as a workaround for http://bugs.python.org/issue3445. 75 | """ 76 | return tuple(a for a in WRAPPER_ASSIGNMENTS if hasattr(fn, a)) 77 | 78 | 79 | def make_middleware_decorator(middleware_class): 80 | def _make_decorator(*m_args, **m_kwargs): 81 | middleware = middleware_class(*m_args, **m_kwargs) 82 | def _decorator(view_func): 83 | def _wrapped_view(request, *args, **kwargs): 84 | if hasattr(middleware, 'process_request'): 85 | result = middleware.process_request(request) 86 | if result is not None: 87 | return result 88 | if hasattr(middleware, 'process_view'): 89 | result = middleware.process_view(request, view_func, args, kwargs) 90 | if result is not None: 91 | return result 92 | try: 93 | response = view_func(request, *args, **kwargs) 94 | except Exception, e: 95 | if hasattr(middleware, 'process_exception'): 96 | result = middleware.process_exception(request, e) 97 | if result is not None: 98 | return result 99 | raise 100 | if hasattr(response, 'render') and callable(response.render): 101 | if hasattr(middleware, 'process_template_response'): 102 | response = middleware.process_template_response(request, response) 103 | # Defer running of process_response until after the template 104 | # has been rendered: 105 | if hasattr(middleware, 'process_response'): 106 | callback = lambda response: middleware.process_response(request, response) 107 | response.add_post_render_callback(callback) 108 | else: 109 | if hasattr(middleware, 'process_response'): 110 | return middleware.process_response(request, response) 111 | return response 112 | return wraps(view_func, assigned=available_attrs(view_func))(_wrapped_view) 113 | return _decorator 114 | return _make_decorator 115 | -------------------------------------------------------------------------------- /airy/utils/hashcompat.py: -------------------------------------------------------------------------------- 1 | """ 2 | The md5 and sha modules are deprecated since Python 2.5, replaced by the 3 | hashlib module containing both hash algorithms. Here, we provide a common 4 | interface to the md5 and sha constructors, depending on system version. 5 | """ 6 | 7 | import sys 8 | if sys.version_info >= (2, 5): 9 | import hashlib 10 | md5_constructor = hashlib.md5 11 | md5_hmac = md5_constructor 12 | sha_constructor = hashlib.sha1 13 | sha_hmac = sha_constructor 14 | else: 15 | import md5 16 | md5_constructor = md5.new 17 | md5_hmac = md5 18 | import sha 19 | sha_constructor = sha.new 20 | sha_hmac = sha 21 | -------------------------------------------------------------------------------- /airy/utils/importlib.py: -------------------------------------------------------------------------------- 1 | # Taken from Python 2.7 with permission from/by the original author. 2 | import sys 3 | 4 | def _resolve_name(name, package, level): 5 | """Return the absolute name of the module to be imported.""" 6 | if not hasattr(package, 'rindex'): 7 | raise ValueError("'package' not set to a string") 8 | dot = len(package) 9 | for x in xrange(level, 1, -1): 10 | try: 11 | dot = package.rindex('.', 0, dot) 12 | except ValueError: 13 | raise ValueError("attempted relative import beyond top-level " 14 | "package") 15 | return "%s.%s" % (package[:dot], name) 16 | 17 | 18 | def import_module(name, package=None): 19 | """Import a module. 20 | 21 | The 'package' argument is required when performing a relative import. It 22 | specifies the package to use as the anchor point from which to resolve the 23 | relative import to an absolute import. 24 | 25 | """ 26 | if name.startswith('.'): 27 | if not package: 28 | raise TypeError("relative imports require the 'package' argument") 29 | level = 0 30 | for character in name: 31 | if character != '.': 32 | break 33 | level += 1 34 | name = _resolve_name(name[level:], package, level) 35 | __import__(name) 36 | return sys.modules[name] 37 | -------------------------------------------------------------------------------- /airy/utils/itercompat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Providing iterator functions that are not in all version of Python we support. 3 | Where possible, we try to use the system-native version and only fall back to 4 | these implementations if necessary. 5 | """ 6 | 7 | import itertools 8 | 9 | # Fallback for Python 2.4, Python 2.5 10 | def product(*args, **kwds): 11 | """ 12 | Taken from http://docs.python.org/library/itertools.html#itertools.product 13 | """ 14 | # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy 15 | # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111 16 | pools = map(tuple, args) * kwds.get('repeat', 1) 17 | result = [[]] 18 | for pool in pools: 19 | result = [x+[y] for x in result for y in pool] 20 | for prod in result: 21 | yield tuple(prod) 22 | 23 | if hasattr(itertools, 'product'): 24 | product = itertools.product 25 | 26 | def is_iterable(x): 27 | "A implementation independent way of checking for iterables" 28 | try: 29 | iter(x) 30 | except TypeError: 31 | return False 32 | else: 33 | return True 34 | 35 | def all(iterable): 36 | for item in iterable: 37 | if not item: 38 | return False 39 | return True 40 | 41 | def any(iterable): 42 | for item in iterable: 43 | if item: 44 | return True 45 | return False 46 | -------------------------------------------------------------------------------- /airy/utils/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from airy.core import mail 4 | 5 | # Make sure a NullHandler is available 6 | # This was added in Python 2.7/3.2 7 | try: 8 | from logging import NullHandler 9 | except ImportError: 10 | class NullHandler(logging.Handler): 11 | def emit(self, record): 12 | pass 13 | 14 | # Make sure that dictConfig is available 15 | # This was added in Python 2.7/3.2 16 | try: 17 | from logging.config import dictConfig 18 | except ImportError: 19 | from airy.utils.dictconfig import dictConfig 20 | 21 | if sys.version_info < (2, 5): 22 | class LoggerCompat(object): 23 | def __init__(self, logger): 24 | self._logger = logger 25 | 26 | def __getattr__(self, name): 27 | val = getattr(self._logger, name) 28 | if callable(val): 29 | def _wrapper(*args, **kwargs): 30 | # Python 2.4 logging module doesn't support 'extra' parameter to 31 | # methods of Logger 32 | kwargs.pop('extra', None) 33 | return val(*args, **kwargs) 34 | return _wrapper 35 | else: 36 | return val 37 | 38 | def getLogger(name=None): 39 | return LoggerCompat(logging.getLogger(name=name)) 40 | else: 41 | getLogger = logging.getLogger 42 | 43 | # Ensure the creation of the Django logger 44 | # with a null handler. This ensures we don't get any 45 | # 'No handlers could be found for logger "django"' messages 46 | logger = getLogger('django') 47 | if not logger.handlers: 48 | logger.addHandler(NullHandler()) 49 | 50 | class AdminEmailHandler(logging.Handler): 51 | def __init__(self, include_html=False): 52 | logging.Handler.__init__(self) 53 | self.include_html = include_html 54 | 55 | """An exception log handler that e-mails log entries to site admins. 56 | 57 | If the request is passed as the first argument to the log record, 58 | request data will be provided in the 59 | """ 60 | def emit(self, record): 61 | import traceback 62 | from airy.core.conf import settings 63 | from airy.views.debug import ExceptionReporter 64 | 65 | try: 66 | if sys.version_info < (2,5): 67 | # A nasty workaround required because Python 2.4's logging 68 | # module doesn't support passing in extra context. 69 | # For this handler, the only extra data we need is the 70 | # request, and that's in the top stack frame. 71 | request = record.exc_info[2].tb_frame.f_locals['request'] 72 | else: 73 | request = record.request 74 | 75 | subject = '%s (%s IP): %s' % ( 76 | record.levelname, 77 | (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS and 'internal' or 'EXTERNAL'), 78 | record.msg 79 | ) 80 | request_repr = repr(request) 81 | except: 82 | subject = '%s: %s' % ( 83 | record.levelname, 84 | record.msg 85 | ) 86 | 87 | request = None 88 | request_repr = "Request repr() unavailable" 89 | 90 | if record.exc_info: 91 | exc_info = record.exc_info 92 | stack_trace = '\n'.join(traceback.format_exception(*record.exc_info)) 93 | else: 94 | exc_info = (None, record.msg, None) 95 | stack_trace = 'No stack trace available' 96 | 97 | message = "%s\n\n%s" % (stack_trace, request_repr) 98 | reporter = ExceptionReporter(request, is_email=True, *exc_info) 99 | html_message = self.include_html and reporter.get_traceback_html() or None 100 | mail.mail_admins(subject, message, fail_silently=True, 101 | html_message=html_message) 102 | -------------------------------------------------------------------------------- /airy/utils/module_loading.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import os 3 | import sys 4 | 5 | 6 | def module_has_submodule(package, module_name): 7 | """See if 'module' is in 'package'.""" 8 | name = ".".join([package.__name__, module_name]) 9 | try: 10 | # None indicates a cached miss; see mark_miss() in Python/import.c. 11 | return sys.modules[name] is not None 12 | except KeyError: 13 | pass 14 | for finder in sys.meta_path: 15 | if finder.find_module(name): 16 | return True 17 | for entry in package.__path__: # No __path__, then not a package. 18 | try: 19 | # Try the cached finder. 20 | finder = sys.path_importer_cache[entry] 21 | if finder is None: 22 | # Implicit import machinery should be used. 23 | try: 24 | file_, _, _ = imp.find_module(module_name, [entry]) 25 | if file_: 26 | file_.close() 27 | return True 28 | except ImportError: 29 | continue 30 | # Else see if the finder knows of a loader. 31 | elif finder.find_module(name): 32 | return True 33 | else: 34 | continue 35 | except KeyError: 36 | # No cached finder, so try and make one. 37 | for hook in sys.path_hooks: 38 | try: 39 | finder = hook(entry) 40 | # XXX Could cache in sys.path_importer_cache 41 | if finder.find_module(name): 42 | return True 43 | else: 44 | # Once a finder is found, stop the search. 45 | break 46 | except ImportError: 47 | # Continue the search for a finder. 48 | continue 49 | else: 50 | # No finder found. 51 | # Try the implicit import machinery if searching a directory. 52 | if os.path.isdir(entry): 53 | try: 54 | file_, _, _ = imp.find_module(module_name, [entry]) 55 | if file_: 56 | file_.close() 57 | return True 58 | except ImportError: 59 | pass 60 | # XXX Could insert None or NullImporter 61 | else: 62 | # Exhausted the search, so the module cannot be found. 63 | return False 64 | -------------------------------------------------------------------------------- /airy/utils/numberformat.py: -------------------------------------------------------------------------------- 1 | from airy.core.conf import settings 2 | from airy.utils.safestring import mark_safe 3 | 4 | 5 | def format(number, decimal_sep, decimal_pos, grouping=0, thousand_sep=''): 6 | """ 7 | Gets a number (as a number or string), and returns it as a string, 8 | using formats definied as arguments: 9 | 10 | * decimal_sep: Decimal separator symbol (for example ".") 11 | * decimal_pos: Number of decimal positions 12 | * grouping: Number of digits in every group limited by thousand separator 13 | * thousand_sep: Thousand separator symbol (for example ",") 14 | 15 | """ 16 | use_grouping = settings.USE_L10N and \ 17 | settings.USE_THOUSAND_SEPARATOR and grouping 18 | # Make the common case fast: 19 | if isinstance(number, int) and not use_grouping and not decimal_pos: 20 | return mark_safe(unicode(number)) 21 | # sign 22 | if float(number) < 0: 23 | sign = '-' 24 | else: 25 | sign = '' 26 | str_number = unicode(number) 27 | if str_number[0] == '-': 28 | str_number = str_number[1:] 29 | # decimal part 30 | if '.' in str_number: 31 | int_part, dec_part = str_number.split('.') 32 | if decimal_pos: 33 | dec_part = dec_part[:decimal_pos] 34 | else: 35 | int_part, dec_part = str_number, '' 36 | if decimal_pos: 37 | dec_part = dec_part + ('0' * (decimal_pos - len(dec_part))) 38 | if dec_part: dec_part = decimal_sep + dec_part 39 | # grouping 40 | if use_grouping: 41 | int_part_gd = '' 42 | for cnt, digit in enumerate(int_part[::-1]): 43 | if cnt and not cnt % grouping: 44 | int_part_gd += thousand_sep 45 | int_part_gd += digit 46 | int_part = int_part_gd[::-1] 47 | return sign + int_part + dec_part 48 | 49 | -------------------------------------------------------------------------------- /airy/utils/safestring.py: -------------------------------------------------------------------------------- 1 | """ 2 | Functions for working with "safe strings": strings that can be displayed safely 3 | without further escaping in HTML. Marking something as a "safe string" means 4 | that the producer of the string has already turned characters that should not 5 | be interpreted by the HTML engine (e.g. '<') into the appropriate entities. 6 | """ 7 | from airy.utils.functional import curry, Promise 8 | 9 | class EscapeData(object): 10 | pass 11 | 12 | class EscapeString(str, EscapeData): 13 | """ 14 | A string that should be HTML-escaped when output. 15 | """ 16 | pass 17 | 18 | class EscapeUnicode(unicode, EscapeData): 19 | """ 20 | A unicode object that should be HTML-escaped when output. 21 | """ 22 | pass 23 | 24 | class SafeData(object): 25 | pass 26 | 27 | class SafeString(str, SafeData): 28 | """ 29 | A string subclass that has been specifically marked as "safe" (requires no 30 | further escaping) for HTML output purposes. 31 | """ 32 | def __add__(self, rhs): 33 | """ 34 | Concatenating a safe string with another safe string or safe unicode 35 | object is safe. Otherwise, the result is no longer safe. 36 | """ 37 | t = super(SafeString, self).__add__(rhs) 38 | if isinstance(rhs, SafeUnicode): 39 | return SafeUnicode(t) 40 | elif isinstance(rhs, SafeString): 41 | return SafeString(t) 42 | return t 43 | 44 | def _proxy_method(self, *args, **kwargs): 45 | """ 46 | Wrap a call to a normal unicode method up so that we return safe 47 | results. The method that is being wrapped is passed in the 'method' 48 | argument. 49 | """ 50 | method = kwargs.pop('method') 51 | data = method(self, *args, **kwargs) 52 | if isinstance(data, str): 53 | return SafeString(data) 54 | else: 55 | return SafeUnicode(data) 56 | 57 | decode = curry(_proxy_method, method = str.decode) 58 | 59 | class SafeUnicode(unicode, SafeData): 60 | """ 61 | A unicode subclass that has been specifically marked as "safe" for HTML 62 | output purposes. 63 | """ 64 | def __add__(self, rhs): 65 | """ 66 | Concatenating a safe unicode object with another safe string or safe 67 | unicode object is safe. Otherwise, the result is no longer safe. 68 | """ 69 | t = super(SafeUnicode, self).__add__(rhs) 70 | if isinstance(rhs, SafeData): 71 | return SafeUnicode(t) 72 | return t 73 | 74 | def _proxy_method(self, *args, **kwargs): 75 | """ 76 | Wrap a call to a normal unicode method up so that we return safe 77 | results. The method that is being wrapped is passed in the 'method' 78 | argument. 79 | """ 80 | method = kwargs.pop('method') 81 | data = method(self, *args, **kwargs) 82 | if isinstance(data, str): 83 | return SafeString(data) 84 | else: 85 | return SafeUnicode(data) 86 | 87 | encode = curry(_proxy_method, method = unicode.encode) 88 | 89 | def mark_safe(s): 90 | """ 91 | Explicitly mark a string as safe for (HTML) output purposes. The returned 92 | object can be used everywhere a string or unicode object is appropriate. 93 | 94 | Can be called multiple times on a single string. 95 | """ 96 | if isinstance(s, SafeData): 97 | return s 98 | if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): 99 | return SafeString(s) 100 | if isinstance(s, (unicode, Promise)): 101 | return SafeUnicode(s) 102 | return SafeString(str(s)) 103 | 104 | def mark_for_escaping(s): 105 | """ 106 | Explicitly mark a string as requiring HTML escaping upon output. Has no 107 | effect on SafeData subclasses. 108 | 109 | Can be called multiple times on a single string (the resulting escaping is 110 | only applied once). 111 | """ 112 | if isinstance(s, (SafeData, EscapeData)): 113 | return s 114 | if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): 115 | return EscapeString(s) 116 | if isinstance(s, (unicode, Promise)): 117 | return EscapeUnicode(s) 118 | return EscapeString(str(s)) 119 | 120 | -------------------------------------------------------------------------------- /airy/utils/simplejson/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Bob Ippolito 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /airy/utils/simplejson/scanner.py: -------------------------------------------------------------------------------- 1 | """JSON token scanner 2 | """ 3 | import re 4 | try: 5 | from simplejson._speedups import make_scanner as c_make_scanner 6 | except ImportError: 7 | c_make_scanner = None 8 | 9 | __all__ = ['make_scanner'] 10 | 11 | NUMBER_RE = re.compile( 12 | r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', 13 | (re.VERBOSE | re.MULTILINE | re.DOTALL)) 14 | 15 | def py_make_scanner(context): 16 | parse_object = context.parse_object 17 | parse_array = context.parse_array 18 | parse_string = context.parse_string 19 | match_number = NUMBER_RE.match 20 | encoding = context.encoding 21 | strict = context.strict 22 | parse_float = context.parse_float 23 | parse_int = context.parse_int 24 | parse_constant = context.parse_constant 25 | object_hook = context.object_hook 26 | 27 | def _scan_once(string, idx): 28 | try: 29 | nextchar = string[idx] 30 | except IndexError: 31 | raise StopIteration 32 | 33 | if nextchar == '"': 34 | return parse_string(string, idx + 1, encoding, strict) 35 | elif nextchar == '{': 36 | return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook) 37 | elif nextchar == '[': 38 | return parse_array((string, idx + 1), _scan_once) 39 | elif nextchar == 'n' and string[idx:idx + 4] == 'null': 40 | return None, idx + 4 41 | elif nextchar == 't' and string[idx:idx + 4] == 'true': 42 | return True, idx + 4 43 | elif nextchar == 'f' and string[idx:idx + 5] == 'false': 44 | return False, idx + 5 45 | 46 | m = match_number(string, idx) 47 | if m is not None: 48 | integer, frac, exp = m.groups() 49 | if frac or exp: 50 | res = parse_float(integer + (frac or '') + (exp or '')) 51 | else: 52 | res = parse_int(integer) 53 | return res, m.end() 54 | elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': 55 | return parse_constant('NaN'), idx + 3 56 | elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': 57 | return parse_constant('Infinity'), idx + 8 58 | elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': 59 | return parse_constant('-Infinity'), idx + 9 60 | else: 61 | raise StopIteration 62 | 63 | return _scan_once 64 | 65 | make_scanner = c_make_scanner or py_make_scanner 66 | -------------------------------------------------------------------------------- /airy/utils/simplejson/tool.py: -------------------------------------------------------------------------------- 1 | r"""Using simplejson from the shell to validate and 2 | pretty-print:: 3 | 4 | $ echo '{"json":"obj"}' | python -msimplejson.tool 5 | { 6 | "json": "obj" 7 | } 8 | $ echo '{ 1.2:3.4}' | python -msimplejson.tool 9 | Expecting property name: line 1 column 2 (char 2) 10 | """ 11 | from airy.utils import simplejson 12 | 13 | def main(): 14 | import sys 15 | if len(sys.argv) == 1: 16 | infile = sys.stdin 17 | outfile = sys.stdout 18 | elif len(sys.argv) == 2: 19 | infile = open(sys.argv[1], 'rb') 20 | outfile = sys.stdout 21 | elif len(sys.argv) == 3: 22 | infile = open(sys.argv[1], 'rb') 23 | outfile = open(sys.argv[2], 'wb') 24 | else: 25 | raise SystemExit("%s [infile [outfile]]" % (sys.argv[0],)) 26 | try: 27 | obj = simplejson.load(infile) 28 | except ValueError, e: 29 | raise SystemExit(e) 30 | simplejson.dump(obj, outfile, sort_keys=True, indent=4) 31 | outfile.write('\n') 32 | 33 | 34 | if __name__ == '__main__': 35 | main() 36 | -------------------------------------------------------------------------------- /airy/utils/stopwords.py: -------------------------------------------------------------------------------- 1 | # Performance note: I benchmarked this code using a set instead of 2 | # a list for the stopwords and was surprised to find that the list 3 | # performed /better/ than the set - maybe because it's only a small 4 | # list. 5 | 6 | stopwords = ''' 7 | i 8 | a 9 | an 10 | are 11 | as 12 | at 13 | be 14 | by 15 | for 16 | from 17 | how 18 | in 19 | is 20 | it 21 | of 22 | on 23 | or 24 | that 25 | the 26 | this 27 | to 28 | was 29 | what 30 | when 31 | where 32 | '''.split() 33 | 34 | def strip_stopwords(sentence): 35 | "Removes stopwords - also normalizes whitespace" 36 | words = sentence.split() 37 | sentence = [] 38 | for word in words: 39 | if word.lower() not in stopwords: 40 | sentence.append(word) 41 | return u' '.join(sentence) 42 | 43 | -------------------------------------------------------------------------------- /airy/utils/synch.py: -------------------------------------------------------------------------------- 1 | """ 2 | Synchronization primitives: 3 | 4 | - reader-writer lock (preference to writers) 5 | 6 | (Contributed to Django by eugene@lazutkin.com) 7 | """ 8 | 9 | try: 10 | import threading 11 | except ImportError: 12 | import dummy_threading as threading 13 | 14 | class RWLock: 15 | """ 16 | Classic implementation of reader-writer lock with preference to writers. 17 | 18 | Readers can access a resource simultaneously. 19 | Writers get an exclusive access. 20 | 21 | API is self-descriptive: 22 | reader_enters() 23 | reader_leaves() 24 | writer_enters() 25 | writer_leaves() 26 | """ 27 | def __init__(self): 28 | self.mutex = threading.RLock() 29 | self.can_read = threading.Semaphore(0) 30 | self.can_write = threading.Semaphore(0) 31 | self.active_readers = 0 32 | self.active_writers = 0 33 | self.waiting_readers = 0 34 | self.waiting_writers = 0 35 | 36 | def reader_enters(self): 37 | self.mutex.acquire() 38 | try: 39 | if self.active_writers == 0 and self.waiting_writers == 0: 40 | self.active_readers += 1 41 | self.can_read.release() 42 | else: 43 | self.waiting_readers += 1 44 | finally: 45 | self.mutex.release() 46 | self.can_read.acquire() 47 | 48 | def reader_leaves(self): 49 | self.mutex.acquire() 50 | try: 51 | self.active_readers -= 1 52 | if self.active_readers == 0 and self.waiting_writers != 0: 53 | self.active_writers += 1 54 | self.waiting_writers -= 1 55 | self.can_write.release() 56 | finally: 57 | self.mutex.release() 58 | 59 | def writer_enters(self): 60 | self.mutex.acquire() 61 | try: 62 | if self.active_writers == 0 and self.waiting_writers == 0 and self.active_readers == 0: 63 | self.active_writers += 1 64 | self.can_write.release() 65 | else: 66 | self.waiting_writers += 1 67 | finally: 68 | self.mutex.release() 69 | self.can_write.acquire() 70 | 71 | def writer_leaves(self): 72 | self.mutex.acquire() 73 | try: 74 | self.active_writers -= 1 75 | if self.waiting_writers != 0: 76 | self.active_writers += 1 77 | self.waiting_writers -= 1 78 | self.can_write.release() 79 | elif self.waiting_readers != 0: 80 | t = self.waiting_readers 81 | self.waiting_readers = 0 82 | self.active_readers += t 83 | while t > 0: 84 | self.can_read.release() 85 | t -= 1 86 | finally: 87 | self.mutex.release() 88 | -------------------------------------------------------------------------------- /airy/utils/timesince.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import time 3 | 4 | from airy.utils.tzinfo import LocalTimezone 5 | from airy.utils.translation import ungettext, ugettext 6 | 7 | def timesince(d, now=None): 8 | """ 9 | Takes two datetime objects and returns the time between d and now 10 | as a nicely formatted string, e.g. "10 minutes". If d occurs after now, 11 | then "0 minutes" is returned. 12 | 13 | Units used are years, months, weeks, days, hours, and minutes. 14 | Seconds and microseconds are ignored. Up to two adjacent units will be 15 | displayed. For example, "2 weeks, 3 days" and "1 year, 3 months" are 16 | possible outputs, but "2 weeks, 3 hours" and "1 year, 5 days" are not. 17 | 18 | Adapted from http://blog.natbat.co.uk/archive/2003/Jun/14/time_since 19 | """ 20 | chunks = ( 21 | (60 * 60 * 24 * 365, lambda n: ungettext('year', 'years', n)), 22 | (60 * 60 * 24 * 30, lambda n: ungettext('month', 'months', n)), 23 | (60 * 60 * 24 * 7, lambda n : ungettext('week', 'weeks', n)), 24 | (60 * 60 * 24, lambda n : ungettext('day', 'days', n)), 25 | (60 * 60, lambda n: ungettext('hour', 'hours', n)), 26 | (60, lambda n: ungettext('minute', 'minutes', n)) 27 | ) 28 | # Convert datetime.date to datetime.datetime for comparison. 29 | if not isinstance(d, datetime.datetime): 30 | d = datetime.datetime(d.year, d.month, d.day) 31 | if now and not isinstance(now, datetime.datetime): 32 | now = datetime.datetime(now.year, now.month, now.day) 33 | 34 | if not now: 35 | if d.tzinfo: 36 | now = datetime.datetime.now(LocalTimezone(d)) 37 | else: 38 | now = datetime.datetime.now() 39 | 40 | # ignore microsecond part of 'd' since we removed it from 'now' 41 | delta = now - (d - datetime.timedelta(0, 0, d.microsecond)) 42 | since = delta.days * 24 * 60 * 60 + delta.seconds 43 | if since <= 0: 44 | # d is in the future compared to now, stop processing. 45 | return u'0 ' + ugettext('minutes') 46 | for i, (seconds, name) in enumerate(chunks): 47 | count = since // seconds 48 | if count != 0: 49 | break 50 | s = ugettext('%(number)d %(type)s') % {'number': count, 'type': name(count)} 51 | if i + 1 < len(chunks): 52 | # Now get the second item 53 | seconds2, name2 = chunks[i + 1] 54 | count2 = (since - (seconds * count)) // seconds2 55 | if count2 != 0: 56 | s += ugettext(', %(number)d %(type)s') % {'number': count2, 'type': name2(count2)} 57 | return s 58 | 59 | def timeuntil(d, now=None): 60 | """ 61 | Like timesince, but returns a string measuring the time until 62 | the given time. 63 | """ 64 | if not now: 65 | if getattr(d, 'tzinfo', None): 66 | now = datetime.datetime.now(LocalTimezone(d)) 67 | else: 68 | now = datetime.datetime.now() 69 | return timesince(now, d) 70 | -------------------------------------------------------------------------------- /airy/utils/translation/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Internationalization support. 3 | """ 4 | import warnings 5 | from os import path 6 | 7 | from airy.utils.encoding import force_unicode 8 | from airy.utils.functional import lazy 9 | from airy.utils.importlib import import_module 10 | 11 | 12 | __all__ = ['gettext', 'gettext_noop', 'gettext_lazy', 'ngettext', 13 | 'ngettext_lazy', 'string_concat', 'activate', 'deactivate', 14 | 'get_language', 'get_language_bidi', 'get_date_formats', 15 | 'get_partial_date_formats', 'check_for_language', 'to_locale', 16 | 'get_language_from_request', 'templatize', 'ugettext', 'ugettext_lazy', 17 | 'ungettext', 'ungettext_lazy', 'pgettext', 'pgettext_lazy', 18 | 'npgettext', 'npgettext_lazy', 'deactivate_all', 'get_language_info'] 19 | 20 | # Here be dragons, so a short explanation of the logic won't hurt: 21 | # We are trying to solve two problems: (1) access settings, in particular 22 | # settings.USE_I18N, as late as possible, so that modules can be imported 23 | # without having to first configure Django, and (2) if some other code creates 24 | # a reference to one of these functions, don't break that reference when we 25 | # replace the functions with their real counterparts (once we do access the 26 | # settings). 27 | 28 | class Trans(object): 29 | """ 30 | The purpose of this class is to store the actual translation function upon 31 | receiving the first call to that function. After this is done, changes to 32 | USE_I18N will have no effect to which function is served upon request. If 33 | your tests rely on changing USE_I18N, you can delete all the functions 34 | from _trans.__dict__. 35 | 36 | Note that storing the function with setattr will have a noticeable 37 | performance effect, as access to the function goes the normal path, 38 | instead of using __getattr__. 39 | """ 40 | 41 | def __getattr__(self, real_name): 42 | from airy.core.conf import settings 43 | if settings.USE_I18N: 44 | from airy.utils.translation import trans_real as trans 45 | # Make sure the project's locale dir isn't in LOCALE_PATHS 46 | if settings.SETTINGS_MODULE is not None: 47 | parts = settings.SETTINGS_MODULE.split('.') 48 | project = import_module(parts[0]) 49 | project_locale_path = path.normpath( 50 | path.join(path.dirname(project.__file__), 'locale')) 51 | normalized_locale_paths = [path.normpath(locale_path) 52 | for locale_path in settings.LOCALE_PATHS] 53 | if (path.isdir(project_locale_path) and 54 | not project_locale_path in normalized_locale_paths): 55 | warnings.warn("Translations in the project directory " 56 | "aren't supported anymore. Use the " 57 | "LOCALE_PATHS setting instead.", 58 | PendingDeprecationWarning) 59 | else: 60 | from airy.utils.translation import trans_null as trans 61 | setattr(self, real_name, getattr(trans, real_name)) 62 | return getattr(trans, real_name) 63 | 64 | _trans = Trans() 65 | 66 | # The Trans class is no more needed, so remove it from the namespace. 67 | del Trans 68 | 69 | def gettext_noop(message): 70 | return _trans.gettext_noop(message) 71 | 72 | ugettext_noop = gettext_noop 73 | 74 | def gettext(message): 75 | return _trans.gettext(message) 76 | 77 | def ngettext(singular, plural, number): 78 | return _trans.ngettext(singular, plural, number) 79 | 80 | def ugettext(message): 81 | return _trans.ugettext(message) 82 | 83 | def ungettext(singular, plural, number): 84 | return _trans.ungettext(singular, plural, number) 85 | 86 | def pgettext(context, message): 87 | return _trans.pgettext(context, message) 88 | 89 | def npgettext(context, singular, plural, number): 90 | return _trans.npgettext(context, singular, plural, number) 91 | 92 | ngettext_lazy = lazy(ngettext, str) 93 | gettext_lazy = lazy(gettext, str) 94 | ungettext_lazy = lazy(ungettext, unicode) 95 | ugettext_lazy = lazy(ugettext, unicode) 96 | pgettext_lazy = lazy(pgettext, unicode) 97 | npgettext_lazy = lazy(npgettext, unicode) 98 | 99 | def activate(language): 100 | return _trans.activate(language) 101 | 102 | def deactivate(): 103 | return _trans.deactivate() 104 | 105 | def get_language(): 106 | return _trans.get_language() 107 | 108 | def get_language_bidi(): 109 | return _trans.get_language_bidi() 110 | 111 | def get_date_formats(): 112 | return _trans.get_date_formats() 113 | 114 | def get_partial_date_formats(): 115 | return _trans.get_partial_date_formats() 116 | 117 | def check_for_language(lang_code): 118 | return _trans.check_for_language(lang_code) 119 | 120 | def to_locale(language): 121 | return _trans.to_locale(language) 122 | 123 | def get_language_from_request(request): 124 | return _trans.get_language_from_request(request) 125 | 126 | def templatize(src, origin=None): 127 | return _trans.templatize(src, origin) 128 | 129 | def deactivate_all(): 130 | return _trans.deactivate_all() 131 | 132 | def _string_concat(*strings): 133 | """ 134 | Lazy variant of string concatenation, needed for translations that are 135 | constructed from multiple parts. 136 | """ 137 | return u''.join([force_unicode(s) for s in strings]) 138 | string_concat = lazy(_string_concat, unicode) 139 | 140 | def get_language_info(lang_code): 141 | from airy.core.conf.locale import LANG_INFO 142 | try: 143 | return LANG_INFO[lang_code] 144 | except KeyError: 145 | raise KeyError("Unknown language code %r." % lang_code) 146 | -------------------------------------------------------------------------------- /airy/utils/translation/trans_null.py: -------------------------------------------------------------------------------- 1 | # These are versions of the functions in django.utils.translation.trans_real 2 | # that don't actually do anything. This is purely for performance, so that 3 | # settings.USE_I18N = False can use this module rather than trans_real.py. 4 | 5 | import warnings 6 | from airy.core.conf import settings 7 | from airy.utils.encoding import force_unicode 8 | from airy.utils.safestring import mark_safe, SafeData 9 | 10 | def ngettext(singular, plural, number): 11 | if number == 1: return singular 12 | return plural 13 | ngettext_lazy = ngettext 14 | 15 | def ungettext(singular, plural, number): 16 | return force_unicode(ngettext(singular, plural, number)) 17 | 18 | def pgettext(context, message): 19 | return ugettext(message) 20 | 21 | def npgettext(context, singular, plural, number): 22 | return ungettext(singular, plural, number) 23 | 24 | activate = lambda x: None 25 | deactivate = deactivate_all = lambda: None 26 | get_language = lambda: settings.LANGUAGE_CODE 27 | get_language_bidi = lambda: settings.LANGUAGE_CODE in settings.LANGUAGES_BIDI 28 | check_for_language = lambda x: True 29 | 30 | # date formats shouldn't be used using gettext anymore. This 31 | # is kept for backward compatibility 32 | TECHNICAL_ID_MAP = { 33 | "DATE_WITH_TIME_FULL": settings.DATETIME_FORMAT, 34 | "DATE_FORMAT": settings.DATE_FORMAT, 35 | "DATETIME_FORMAT": settings.DATETIME_FORMAT, 36 | "TIME_FORMAT": settings.TIME_FORMAT, 37 | "YEAR_MONTH_FORMAT": settings.YEAR_MONTH_FORMAT, 38 | "MONTH_DAY_FORMAT": settings.MONTH_DAY_FORMAT, 39 | } 40 | 41 | def gettext(message): 42 | result = TECHNICAL_ID_MAP.get(message, message) 43 | if isinstance(message, SafeData): 44 | return mark_safe(result) 45 | return result 46 | 47 | def ugettext(message): 48 | return force_unicode(gettext(message)) 49 | 50 | gettext_noop = gettext_lazy = _ = gettext 51 | 52 | def to_locale(language): 53 | p = language.find('-') 54 | if p >= 0: 55 | return language[:p].lower()+'_'+language[p+1:].upper() 56 | else: 57 | return language.lower() 58 | 59 | def get_language_from_request(request): 60 | return settings.LANGUAGE_CODE 61 | 62 | # get_date_formats and get_partial_date_formats aren't used anymore by Django 63 | # but are kept for backward compatibility. 64 | def get_date_formats(): 65 | warnings.warn( 66 | '`django.utils.translation.get_date_formats` is deprecated. ' 67 | 'Please update your code to use the new i18n aware formatting.', 68 | DeprecationWarning 69 | ) 70 | return settings.DATE_FORMAT, settings.DATETIME_FORMAT, settings.TIME_FORMAT 71 | 72 | def get_partial_date_formats(): 73 | warnings.warn( 74 | '`django.utils.translation.get_partial_date_formats` is deprecated. ' 75 | 'Please update your code to use the new i18n aware formatting.', 76 | DeprecationWarning 77 | ) 78 | return settings.YEAR_MONTH_FORMAT, settings.MONTH_DAY_FORMAT 79 | -------------------------------------------------------------------------------- /airy/utils/tzinfo.py: -------------------------------------------------------------------------------- 1 | "Implementation of tzinfo classes for use with datetime.datetime." 2 | 3 | import time 4 | from datetime import timedelta, tzinfo 5 | from airy.utils.encoding import smart_unicode, smart_str, DEFAULT_LOCALE_ENCODING 6 | 7 | class FixedOffset(tzinfo): 8 | "Fixed offset in minutes east from UTC." 9 | def __init__(self, offset): 10 | if isinstance(offset, timedelta): 11 | self.__offset = offset 12 | offset = self.__offset.seconds // 60 13 | else: 14 | self.__offset = timedelta(minutes=offset) 15 | 16 | sign = offset < 0 and '-' or '+' 17 | self.__name = u"%s%02d%02d" % (sign, abs(offset) / 60., abs(offset) % 60) 18 | 19 | def __repr__(self): 20 | return self.__name 21 | 22 | def utcoffset(self, dt): 23 | return self.__offset 24 | 25 | def tzname(self, dt): 26 | return self.__name 27 | 28 | def dst(self, dt): 29 | return timedelta(0) 30 | 31 | class LocalTimezone(tzinfo): 32 | "Proxy timezone information from time module." 33 | def __init__(self, dt): 34 | tzinfo.__init__(self) 35 | self._tzname = self.tzname(dt) 36 | 37 | def __repr__(self): 38 | return smart_str(self._tzname) 39 | 40 | def utcoffset(self, dt): 41 | if self._isdst(dt): 42 | return timedelta(seconds=-time.altzone) 43 | else: 44 | return timedelta(seconds=-time.timezone) 45 | 46 | def dst(self, dt): 47 | if self._isdst(dt): 48 | return timedelta(seconds=-time.altzone) - timedelta(seconds=-time.timezone) 49 | else: 50 | return timedelta(0) 51 | 52 | def tzname(self, dt): 53 | try: 54 | return smart_unicode(time.tzname[self._isdst(dt)], 55 | DEFAULT_LOCALE_ENCODING) 56 | except UnicodeDecodeError: 57 | return None 58 | 59 | def _isdst(self, dt): 60 | tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, -1) 61 | try: 62 | stamp = time.mktime(tt) 63 | except (OverflowError, ValueError): 64 | # 32 bit systems can't handle dates after Jan 2038, and certain 65 | # systems can't handle dates before ~1901-12-01: 66 | # 67 | # >>> time.mktime((1900, 1, 13, 0, 0, 0, 0, 0, 0)) 68 | # OverflowError: mktime argument out of range 69 | # >>> time.mktime((1850, 1, 13, 0, 0, 0, 0, 0, 0)) 70 | # ValueError: year out of range 71 | # 72 | # In this case, we fake the date, because we only care about the 73 | # DST flag. 74 | tt = (2037,) + tt[1:] 75 | stamp = time.mktime(tt) 76 | tt = time.localtime(stamp) 77 | return tt.tm_isdst > 0 78 | -------------------------------------------------------------------------------- /airy/utils/unittest/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | unittest2 3 | 4 | unittest2 is a backport of the new features added to the unittest testing 5 | framework in Python 2.7. It is tested to run on Python 2.4 - 2.6. 6 | 7 | To use unittest2 instead of unittest simply replace ``import unittest`` with 8 | ``import unittest2``. 9 | 10 | 11 | Copyright (c) 1999-2003 Steve Purcell 12 | Copyright (c) 2003-2010 Python Software Foundation 13 | This module is free software, and you may redistribute it and/or modify 14 | it under the same terms as Python itself, so long as this copyright message 15 | and disclaimer are retained in their original form. 16 | 17 | IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, 18 | SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF 19 | THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 20 | DAMAGE. 21 | 22 | THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, 25 | AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, 26 | SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 27 | """ 28 | 29 | import sys 30 | 31 | # Django hackery to load the appropriate version of unittest 32 | 33 | try: 34 | # check the system path first 35 | from unittest2 import * 36 | except ImportError: 37 | if sys.version_info >= (2,7): 38 | # unittest2 features are native in Python 2.7 39 | from unittest import * 40 | else: 41 | # otherwise use our bundled version 42 | __all__ = ['TestResult', 'TestCase', 'TestSuite', 43 | 'TextTestRunner', 'TestLoader', 'FunctionTestCase', 'main', 44 | 'defaultTestLoader', 'SkipTest', 'skip', 'skipIf', 'skipUnless', 45 | 'expectedFailure', 'TextTestResult', '__version__', 'collector'] 46 | 47 | __version__ = '0.5.1' 48 | 49 | # Expose obsolete functions for backwards compatibility 50 | __all__.extend(['getTestCaseNames', 'makeSuite', 'findTestCases']) 51 | 52 | 53 | from airy.utils.unittest.collector import collector 54 | from airy.utils.unittest.result import TestResult 55 | from airy.utils.unittest.case import \ 56 | TestCase, FunctionTestCase, SkipTest, skip, skipIf,\ 57 | skipUnless, expectedFailure 58 | 59 | from airy.utils.unittest.suite import BaseTestSuite, TestSuite 60 | from airy.utils.unittest.loader import \ 61 | TestLoader, defaultTestLoader, makeSuite, getTestCaseNames,\ 62 | findTestCases 63 | 64 | from airy.utils.unittest.main import TestProgram, main, main_ 65 | from airy.utils.unittest.runner import TextTestRunner, TextTestResult 66 | 67 | try: 68 | from airy.utils.unittest.signals import\ 69 | installHandler, registerResult, removeResult, removeHandler 70 | except ImportError: 71 | # Compatibility with platforms that don't have the signal module 72 | pass 73 | else: 74 | __all__.extend(['installHandler', 'registerResult', 'removeResult', 75 | 'removeHandler']) 76 | 77 | # deprecated 78 | _TextTestResult = TextTestResult 79 | 80 | __unittest = True 81 | -------------------------------------------------------------------------------- /airy/utils/unittest/__main__.py: -------------------------------------------------------------------------------- 1 | """Main entry point""" 2 | 3 | import sys 4 | if sys.argv[0].endswith("__main__.py"): 5 | sys.argv[0] = "unittest2" 6 | 7 | __unittest = True 8 | 9 | from airy.utils.unittest.main import main_ 10 | main_() 11 | -------------------------------------------------------------------------------- /airy/utils/unittest/collector.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from airy.utils.unittest.loader import defaultTestLoader 4 | 5 | def collector(): 6 | # import __main__ triggers code re-execution 7 | __main__ = sys.modules['__main__'] 8 | setupDir = os.path.abspath(os.path.dirname(__main__.__file__)) 9 | return defaultTestLoader.discover(setupDir) 10 | -------------------------------------------------------------------------------- /airy/utils/unittest/compatibility.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | try: 5 | from functools import wraps 6 | except ImportError: 7 | # only needed for Python 2.4 8 | def wraps(_): 9 | def _wraps(func): 10 | return func 11 | return _wraps 12 | 13 | __unittest = True 14 | 15 | def _relpath_nt(path, start=os.path.curdir): 16 | """Return a relative version of a path""" 17 | 18 | if not path: 19 | raise ValueError("no path specified") 20 | start_list = os.path.abspath(start).split(os.path.sep) 21 | path_list = os.path.abspath(path).split(os.path.sep) 22 | if start_list[0].lower() != path_list[0].lower(): 23 | unc_path, rest = os.path.splitunc(path) 24 | unc_start, rest = os.path.splitunc(start) 25 | if bool(unc_path) ^ bool(unc_start): 26 | raise ValueError("Cannot mix UNC and non-UNC paths (%s and %s)" 27 | % (path, start)) 28 | else: 29 | raise ValueError("path is on drive %s, start on drive %s" 30 | % (path_list[0], start_list[0])) 31 | # Work out how much of the filepath is shared by start and path. 32 | for i in range(min(len(start_list), len(path_list))): 33 | if start_list[i].lower() != path_list[i].lower(): 34 | break 35 | else: 36 | i += 1 37 | 38 | rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] 39 | if not rel_list: 40 | return os.path.curdir 41 | return os.path.join(*rel_list) 42 | 43 | # default to posixpath definition 44 | def _relpath_posix(path, start=os.path.curdir): 45 | """Return a relative version of a path""" 46 | 47 | if not path: 48 | raise ValueError("no path specified") 49 | 50 | start_list = os.path.abspath(start).split(os.path.sep) 51 | path_list = os.path.abspath(path).split(os.path.sep) 52 | 53 | # Work out how much of the filepath is shared by start and path. 54 | i = len(os.path.commonprefix([start_list, path_list])) 55 | 56 | rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] 57 | if not rel_list: 58 | return os.path.curdir 59 | return os.path.join(*rel_list) 60 | 61 | if os.path is sys.modules.get('ntpath'): 62 | relpath = _relpath_nt 63 | else: 64 | relpath = _relpath_posix 65 | -------------------------------------------------------------------------------- /airy/utils/unittest/signals.py: -------------------------------------------------------------------------------- 1 | import signal 2 | import weakref 3 | 4 | from airy.utils.unittest.compatibility import wraps 5 | 6 | __unittest = True 7 | 8 | 9 | class _InterruptHandler(object): 10 | def __init__(self, default_handler): 11 | self.called = False 12 | self.default_handler = default_handler 13 | 14 | def __call__(self, signum, frame): 15 | installed_handler = signal.getsignal(signal.SIGINT) 16 | if installed_handler is not self: 17 | # if we aren't the installed handler, then delegate immediately 18 | # to the default handler 19 | self.default_handler(signum, frame) 20 | 21 | if self.called: 22 | self.default_handler(signum, frame) 23 | self.called = True 24 | for result in _results.keys(): 25 | result.stop() 26 | 27 | _results = weakref.WeakKeyDictionary() 28 | def registerResult(result): 29 | _results[result] = 1 30 | 31 | def removeResult(result): 32 | return bool(_results.pop(result, None)) 33 | 34 | _interrupt_handler = None 35 | def installHandler(): 36 | global _interrupt_handler 37 | if _interrupt_handler is None: 38 | default_handler = signal.getsignal(signal.SIGINT) 39 | _interrupt_handler = _InterruptHandler(default_handler) 40 | signal.signal(signal.SIGINT, _interrupt_handler) 41 | 42 | 43 | def removeHandler(method=None): 44 | if method is not None: 45 | @wraps(method) 46 | def inner(*args, **kwargs): 47 | initial = signal.getsignal(signal.SIGINT) 48 | removeHandler() 49 | try: 50 | return method(*args, **kwargs) 51 | finally: 52 | signal.signal(signal.SIGINT, initial) 53 | return inner 54 | 55 | global _interrupt_handler 56 | if _interrupt_handler is not None: 57 | signal.signal(signal.SIGINT, _interrupt_handler.default_handler) 58 | -------------------------------------------------------------------------------- /airy/utils/unittest/util.py: -------------------------------------------------------------------------------- 1 | """Various utility functions.""" 2 | 3 | __unittest = True 4 | 5 | 6 | _MAX_LENGTH = 80 7 | def safe_repr(obj, short=False): 8 | try: 9 | result = repr(obj) 10 | except Exception: 11 | result = object.__repr__(obj) 12 | if not short or len(result) < _MAX_LENGTH: 13 | return result 14 | return result[:_MAX_LENGTH] + ' [truncated]...' 15 | 16 | def safe_str(obj): 17 | try: 18 | return str(obj) 19 | except Exception: 20 | return object.__str__(obj) 21 | 22 | def strclass(cls): 23 | return "%s.%s" % (cls.__module__, cls.__name__) 24 | 25 | def sorted_list_difference(expected, actual): 26 | """Finds elements in only one or the other of two, sorted input lists. 27 | 28 | Returns a two-element tuple of lists. The first list contains those 29 | elements in the "expected" list but not in the "actual" list, and the 30 | second contains those elements in the "actual" list but not in the 31 | "expected" list. Duplicate elements in either input list are ignored. 32 | """ 33 | i = j = 0 34 | missing = [] 35 | unexpected = [] 36 | while True: 37 | try: 38 | e = expected[i] 39 | a = actual[j] 40 | if e < a: 41 | missing.append(e) 42 | i += 1 43 | while expected[i] == e: 44 | i += 1 45 | elif e > a: 46 | unexpected.append(a) 47 | j += 1 48 | while actual[j] == a: 49 | j += 1 50 | else: 51 | i += 1 52 | try: 53 | while expected[i] == e: 54 | i += 1 55 | finally: 56 | j += 1 57 | while actual[j] == a: 58 | j += 1 59 | except IndexError: 60 | missing.extend(expected[i:]) 61 | unexpected.extend(actual[j:]) 62 | break 63 | return missing, unexpected 64 | 65 | def unorderable_list_difference(expected, actual, ignore_duplicate=False): 66 | """Same behavior as sorted_list_difference but 67 | for lists of unorderable items (like dicts). 68 | 69 | As it does a linear search per item (remove) it 70 | has O(n*n) performance. 71 | """ 72 | missing = [] 73 | unexpected = [] 74 | while expected: 75 | item = expected.pop() 76 | try: 77 | actual.remove(item) 78 | except ValueError: 79 | missing.append(item) 80 | if ignore_duplicate: 81 | for lst in expected, actual: 82 | try: 83 | while True: 84 | lst.remove(item) 85 | except ValueError: 86 | pass 87 | if ignore_duplicate: 88 | while actual: 89 | item = actual.pop() 90 | unexpected.append(item) 91 | try: 92 | while True: 93 | actual.remove(item) 94 | except ValueError: 95 | pass 96 | return missing, unexpected 97 | 98 | # anything left in actual is unexpected 99 | return missing, actual 100 | -------------------------------------------------------------------------------- /airy/utils/version.py: -------------------------------------------------------------------------------- 1 | import airy 2 | import os.path 3 | import re 4 | 5 | def get_svn_revision(path=None): 6 | """ 7 | Returns the SVN revision in the form SVN-XXXX, 8 | where XXXX is the revision number. 9 | 10 | Returns SVN-unknown if anything goes wrong, such as an unexpected 11 | format of internal SVN files. 12 | 13 | If path is provided, it should be a directory whose SVN info you want to 14 | inspect. If it's not provided, this will use the root django/ package 15 | directory. 16 | """ 17 | rev = None 18 | if path is None: 19 | path = django.__path__[0] 20 | entries_path = '%s/.svn/entries' % path 21 | 22 | try: 23 | entries = open(entries_path, 'r').read() 24 | except IOError: 25 | pass 26 | else: 27 | # Versions >= 7 of the entries file are flat text. The first line is 28 | # the version number. The next set of digits after 'dir' is the revision. 29 | if re.match('(\d+)', entries): 30 | rev_match = re.search('\d+\s+dir\s+(\d+)', entries) 31 | if rev_match: 32 | rev = rev_match.groups()[0] 33 | # Older XML versions of the file specify revision as an attribute of 34 | # the first entries node. 35 | else: 36 | from xml.dom import minidom 37 | dom = minidom.parse(entries_path) 38 | rev = dom.getElementsByTagName('entry')[0].getAttribute('revision') 39 | 40 | if rev: 41 | return u'SVN-%s' % rev 42 | return u'SVN-unknown' 43 | -------------------------------------------------------------------------------- /airy/utils/xmlutils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities for XML generation/parsing. 3 | """ 4 | 5 | from xml.sax.saxutils import XMLGenerator 6 | 7 | class SimplerXMLGenerator(XMLGenerator): 8 | def addQuickElement(self, name, contents=None, attrs=None): 9 | "Convenience method for adding an element with no children" 10 | if attrs is None: attrs = {} 11 | self.startElement(name, attrs) 12 | if contents is not None: 13 | self.characters(contents) 14 | self.endElement(name) 15 | -------------------------------------------------------------------------------- /doc/changelog.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Changes in v0.1 6 | ================= 7 | 8 | - Added new markup tools: sanitize(), strip_tags() and linkify() 9 | - New admin script: airy-admin.py 10 | - PyPI package 11 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Airy documentation master file, created by 2 | sphinx-quickstart on Tue Jan 31 11:11:36 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | =========== 7 | Airy Manual 8 | =========== 9 | 10 | **Airy** is a Web application development framework. To install it, run: 11 | 12 | .. code-block:: console 13 | 14 | # pip install airy 15 | 16 | :doc:`intro` 17 | About Airy and how it can be used. 18 | 19 | :doc:`tutorial` 20 | Start here for a quick hands-on experience. 21 | 22 | :doc:`reference` 23 | Detailed documentation. 24 | 25 | Community 26 | --------- 27 | 28 | If you spotted a bug or would like to request a feature, please head to our GitHub page: 29 | 30 | http://github.com/letolab/airy 31 | 32 | If you would like to contribute, fork airy on GitHub and send us a pull request. 33 | 34 | .. toctree:: 35 | :hidden: 36 | 37 | intro 38 | tutorial 39 | reference 40 | changelog 41 | 42 | Changes 43 | ------- 44 | See the :doc:`changelog` for a full list of changes. 45 | 46 | Indices and tables 47 | ------------------ 48 | 49 | * :ref:`genindex` 50 | * :ref:`modindex` 51 | * :ref:`search` 52 | 53 | -------------------------------------------------------------------------------- /doc/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | Airy is a new Web application development framework. Contrast to most currently available frameworks, 5 | Airy doesn't use the standard notion of HTTP requests and pages. Instead, it makes use of WebSockets 6 | and provides a set of tools to let you focus on the interface, not content delivery. 7 | 8 | Currently Airy supports MongoDB only. We plan to provide support for other NoSQL databases, 9 | but we have no plans for supporting SQL. 10 | 11 | Airy includes or otherwise uses a set of tools and third-party libraries. For a full list of 12 | modules see `Acknowledgement`_ 13 | 14 | WebSockets 15 | ---------- 16 | 17 | Airy uses WebSockets for all client-server communication. 18 | 19 | On older browsers it degrades to AJAX polling or Flash (or HTML File), thanks to 20 | `Socket.io `_ and `TornadIO2 `_ 21 | that are used throughout Airy. 22 | 23 | This means that Airy comes with real-time support built-in which works on any modern browser. 24 | 25 | Database 26 | -------- 27 | 28 | Only MongoDB is currently supported. We will appreciate any help with adding support for other database engines, 29 | but we have no plans for supporting SQL-based engines or any other relational databases. 30 | 31 | Airy comes with a simple ORM, which is provided by `MongoEngine `_ 32 | 33 | Please refer to the `MongoEngine Documentation `_ for details on how to use the database layer. 34 | 35 | Interface 36 | --------- 37 | 38 | By default, Airy includes `Twitter Bootstrap `_ 39 | 40 | Airy also includes a small JavaScript library that is responsible for establishing initial Socket.io connection. 41 | Additionally, it modifies web page content and sends all requests via the connection instead of initiating a plain HTTP request. 42 | 43 | Acknowledgement 44 | --------------- 45 | 46 | Airy is built on top of and relies on the following tools and libraries: 47 | 48 | * `Tornado `_ 49 | * `MongoEngine `_ 50 | * `Socket.io `_ 51 | * `TornadIO2 `_ 52 | * `jQuery `_ 53 | * `jQuery History Plugin `_ 54 | * `jQuery Cookie Plugin `_ 55 | 56 | Additionally, a part of `Django `_ has been ported to Airy, specifically Django Forms, 57 | File handling library, markup utils and more. 58 | 59 | -------------------------------------------------------------------------------- /doc/reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ==================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :glob: 8 | 9 | reference/* 10 | -------------------------------------------------------------------------------- /doc/reference/00_project.rst: -------------------------------------------------------------------------------- 1 | Project Structure 2 | ==================================== 3 | 4 | A typical Airy project has the following structure: 5 | 6 | .. code-block:: none 7 | 8 | myproject/ 9 | myapp/ 10 | __init__.py 11 | handlers.py 12 | models.py 13 | urls.py 14 | static/ 15 | templates/ 16 | __init__.py 17 | flashpolicy.xml 18 | manage.py 19 | requirements.pip 20 | settings.py 21 | 22 | Airy expects every app enabled in ``settings.installed_apps`` to have at least those 4 files under ``myapp/`` from above. 23 | 24 | * ``myapp/handlers.py`` 25 | 26 | A file containing all your handlers for ``myapp``. 27 | It may also have a 'root' handler subclassing :py:class:`~airy.core.web.AiryRequestHandler` 28 | 29 | * ``myapp/models.py`` 30 | 31 | Your database models go here. See :doc:`Database <02_database>` for more details. 32 | 33 | * ``myapp/urls.py`` 34 | 35 | Define URLs for your app there. For example a typical file may look like this: 36 | 37 | .. code-block:: python 38 | 39 | # snippet from users/urls.py: 40 | 41 | from handlers import * 42 | 43 | urlpatterns = [ 44 | (r"/.*", IndexHandler), # root handler to accept old-style HTTP requests 45 | 46 | (r"/", HomeHandler), 47 | (r"/accounts/login", AccountsLoginHandler), 48 | 49 | # ... 50 | 51 | ] 52 | 53 | * ``static/`` 54 | 55 | Static files (JavaScript, CSS, images, user media, etc.) go here. You can change the default path 56 | by specifying ``settings.static_path`` 57 | 58 | * ``templates/`` 59 | 60 | Templates here. Change via ``settings.template_path`` 61 | 62 | * ``flashpolicy.xml`` 63 | 64 | Defines Flash policy for older browsers (not supporting WebSockets). It is possible that you 65 | will never need to change this file. It is, however, required by Airy. 66 | 67 | * ``manage.py`` 68 | 69 | A basic python script that invokes **Airy**. You wouldn't normally need to change it. 70 | It comes with every new project created via ``airy-admin.py`` 71 | 72 | * ``settings.py`` 73 | 74 | Your project's settings file. Technically it's just a python file, so you can use it 75 | to override default settings (like ``settings.static_path`` or ``settings.port``) or 76 | just specify it at run time as an argument. 77 | 78 | See :doc:`Command-line Interface <05_console>` for details. 79 | -------------------------------------------------------------------------------- /doc/reference/01_handlers.rst: -------------------------------------------------------------------------------- 1 | Handlers 2 | ==================================== 3 | 4 | Overview 5 | -------- 6 | 7 | In every project, you would normally have a single handler subclassing 8 | :py:class:`~airy.core.web.AiryRequestHandler` to process ordinary HTTP requests. 9 | 10 | This handler should be responsible for rendering basic HTML page and establishing 11 | Socket.io connection. 12 | 13 | If you've used ``airy-admin.py startproject`` to create your project, you should already 14 | have such handler in ``users/handlers.py`` and a bunch of other Socket.io handlers (subclassing 15 | :py:class:`~airy.core.web.AiryHandler`) 16 | 17 | When a Socket.io connection is established, Airy sends all data over that connection and 18 | emulates the standard GET/POST behaviour for ordinary links and all forms in your project. 19 | 20 | You safely push HTML data to the client and Airy will adjust forms and links to use Socket.io 21 | connection instead of making an HTTP request every time. 22 | 23 | If you creating your project manually from scratch, you may want add a 'core' app (such as users) 24 | with the following code to implement the functionality: 25 | 26 | .. code-block:: python 27 | 28 | from airy.core.web import AiryHandler, AiryRequestHandler 29 | 30 | class IndexHandler(AiryRequestHandler): 31 | """ 32 | This is a standard old-style HTTP handler. 33 | 34 | When a client makes initial HTTP request it will render the basic page 35 | containing all the required elements. 36 | """ 37 | def get(self): 38 | self.render("base.html") 39 | 40 | """ 41 | The file 'base.html' may look like this: 42 | 43 | 44 | 45 | My Airy Project 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
    58 |
    59 | 60 | 61 | 62 | """ 63 | 64 | 65 | class HomeHandler(AiryHandler): 66 | """ 67 | This is an example of a Socket.io handler where all data is 68 | """ 69 | def get(self): 70 | self.render("#content", "index.html", user=self.get_current_user()) 71 | 72 | """ 73 | The file 'index.html' may look like this: 74 | 75 |

    Hello, {{ user.username }}!

    76 | 77 | """ 78 | 79 | Note: 80 | 81 | The JavaScript files (included in the base.html example above) are responsible for managing 82 | Socket.io communication. If you don't include them, your project will work just like an ordinary 83 | Tornado website. No handlers subclassing AiryHandler will be invoked - Airy will just ignore them. 84 | 85 | 86 | Class Reference 87 | --------------- 88 | 89 | .. autoclass:: airy.core.web.AiryRequestHandler 90 | :members: 91 | :inherited-members: 92 | :member-order: bysource 93 | 94 | .. autoclass:: airy.core.web.AiryHandler 95 | :members: 96 | :inherited-members: 97 | :member-order: bysource 98 | 99 | .. attribute:: site 100 | 101 | An :py:class:`~airy.core.web.AirySite` object containing all current connections. Each 102 | AiryHandler refers to the same AirySite, which allows you to send data between site users. 103 | See :py:attr:`~AirySite.connections` for more details. 104 | 105 | .. attribute:: arguments 106 | 107 | A dictionary containing all arguments sent with the request (e.g. a form data for a POST-like request). 108 | 109 | .. attribute:: files 110 | 111 | A dictionary with all the files sent with the request. Each file is an 112 | ``airy.core.files.uploadedfile.SimpleUploadedFile`` instance. 113 | 114 | .. attribute:: connection 115 | 116 | Current connection object with all related metadata. 117 | 118 | .. autoclass:: airy.core.web.AirySite 119 | :members: 120 | :inherited-members: 121 | :member-order: bysource 122 | 123 | .. attribute:: connections 124 | 125 | A ``set`` with all current connections. Each connection has a ``state`` attribute, which 126 | contains the most recently requested URL. You can iterate over the connections to send data 127 | between users, for example: 128 | 129 | .. code-block:: python 130 | 131 | class MessagesHandler(AiryHandler): 132 | 133 | # ... 134 | 135 | def post(self, path): 136 | "Send a message to all current visitors" 137 | 138 | form = MessageForm(self.get_flat_arguments()) 139 | if form.is_valid(): 140 | message = form.save(self) 141 | 142 | for conn in self.site.connections: 143 | if conn.state == '/chat/' or conn == self.connection: 144 | conn.append( 145 | '#message-thread', 146 | self.render_string('messaging/message.html', message=reply) 147 | ) 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /doc/reference/02_database.rst: -------------------------------------------------------------------------------- 1 | Database 2 | ==================================== 3 | 4 | Airy currently supports MongoDB only. By default, it connects to the local MongoDB server 5 | and uses the database specified in ``settings.database_name``. 6 | 7 | Every app should define its models in ``models.py``. You may keep an empty ``models.py`` file if 8 | your app doesn't need to store any data. 9 | 10 | An ordinary ``models.py`` file may look like this: 11 | 12 | .. code-block:: python 13 | 14 | from airy.core.db import * 15 | 16 | class Comment(Action): 17 | text = StringField(required=True) 18 | 19 | 20 | class Post(Document): 21 | title = StringField(max_length=128, required=True) 22 | text = StringField(required=True) 23 | is_published = BooleanField(default=False) 24 | comments = ListField(ReferenceField(Comment)) 25 | 26 | def __unicode__(self): 27 | return self.title 28 | 29 | 30 | 31 | It relies on `MongoEngine `_ for ORM. 32 | 33 | Please refer to `MongoEngine Documentation `_ for detailed documentation. 34 | 35 | -------------------------------------------------------------------------------- /doc/reference/03_settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ==================================== 3 | 4 | .. automodule:: airy.core.conf 5 | :members: 6 | :inherited-members: 7 | :member-order: bysource 8 | 9 | -------------------------------------------------------------------------------- /doc/reference/05_console.rst: -------------------------------------------------------------------------------- 1 | Command-line Interface 2 | ==================================== 3 | 4 | airy-admin.py 5 | ------------- 6 | 7 | Airy comes with a handy admin script: ``airy-admin.py`` 8 | 9 | You would normally use it to create a new project or a new app within an existing project. 10 | 11 | **Available commands:** 12 | 13 | * ``startproject :`` 14 | 15 | Creates a new project in a folder named 16 | 17 | * ``startapp :`` 18 | 19 | Creates a new app in a folder named 20 | 21 | * ``help:`` 22 | 23 | Displays this help 24 | 25 | 26 | manage.py 27 | --------- 28 | 29 | When you create a new project using ``airy-admin.py`` Airy automatically adds a ``manage.py`` script. 30 | 31 | **Available commands:** 32 | 33 | * ``run [OPTIONS]`` 34 | 35 | Starts Tornado web server. For a full list of options type ``python manage.py help`` 36 | 37 | * ``runserver [OPTIONS]`` 38 | 39 | An alias of ``run``. 40 | 41 | * ``shell [OPTIONS]`` 42 | 43 | Sets up the environment (connects to the DB, imports modules) and starts IPython shell. 44 | 45 | * ``help`` 46 | 47 | Print a list of available options. 48 | -------------------------------------------------------------------------------- /doc/tutorial.rst: -------------------------------------------------------------------------------- 1 | Quick Tutorial 2 | ==================================== 3 | 4 | Installation 5 | ------------ 6 | 7 | To install **Airy**, run: 8 | 9 | .. code-block:: console 10 | 11 | # pip install airy 12 | 13 | Airy needs a running `MongoDB `_ server. Please refer to the relevant page in MongoDB docs 14 | regarding MongoDB installation. 15 | 16 | Creating new project 17 | -------------------- 18 | 19 | Once you have installed Airy, you should have the ``airy-admin.py`` script available. 20 | 21 | Open a Terminal, navigate to some directory where you want to create a new project and type: 22 | 23 | .. code-block:: console 24 | 25 | $ airy-admin.py startproject project_name 26 | $ cd project_name 27 | 28 | 29 | Configuration 30 | ------------- 31 | 32 | Edit the ``settings.py`` file in the project directory. 33 | 34 | Make sure you change the ``database_name`` and the ``cookie_secret``: 35 | 36 | .. code-block:: python 37 | 38 | ... 39 | 40 | database_name = 'airydb' # replace with your DB name 41 | 42 | ... 43 | 44 | cookie_secret = 'airy_secret' # replace with yours 45 | 46 | ... 47 | 48 | 49 | Starting up 50 | ----------- 51 | 52 | Run ``update_ve`` to download and build all the required modules, then start your project: 53 | 54 | .. code-block:: console 55 | 56 | $ python manage.py update_ve 57 | $ python manage.py run 58 | 59 | You should see something like this: 60 | 61 | .. code-block:: none 62 | 63 | [I 120131 11:30:01 server:86] Starting up tornadio server on port '8000' 64 | [I 120131 11:30:01 server:93] Starting Flash policy server on port '8043' 65 | [I 120131 11:30:01 server:103] Entering IOLoop... 66 | 67 | Then open your browser and navigate to http://localhost:8000/ 68 | 69 | Hopefully you will see the default Airy welcome page. 70 | 71 | 72 | Next Steps 73 | ---------- 74 | 75 | You may want to check out the ``users`` app (included by default in every new project). 76 | 77 | If you want to start a new app use: 78 | 79 | .. code-block:: console 80 | 81 | $ airy-admin.py startapp 82 | 83 | This will create a new folder ``test/`` in the current directory. Don't forget to add 84 | your new app to ``settings.installed_apps``. 85 | 86 | See :doc:`API Reference ` for documentation on other . 87 | 88 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | def read(fname): 5 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 6 | 7 | setup( 8 | name = "airy", 9 | version = "0.3.1", 10 | author = "LetoLab Ltd", 11 | author_email = "team@letolab.com", 12 | description = ("Web Application Framework"), 13 | long_description=read('README'), 14 | license = "BSD", 15 | keywords = "web development websockets", 16 | url = "http://airy.letolab.com", 17 | packages=find_packages(), 18 | include_package_data=True, 19 | requires=[ 20 | 'simplejson', 21 | 'ipython', 22 | ], 23 | scripts=[ 24 | 'airy/bin/airy-admin.py', 25 | ], 26 | classifiers=[ 27 | "Programming Language :: Python", 28 | "Topic :: Internet :: WWW/HTTP", 29 | "Topic :: Utilities", 30 | "License :: OSI Approved :: BSD License", 31 | "Development Status :: 3 - Alpha", 32 | ], 33 | ) 34 | --------------------------------------------------------------------------------