From ae078728c8bc1c7d4412c4cb19b6802e9b7e8f74 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Sun, 14 Jun 2009 19:51:50 -0400 Subject: initial appengine setup; BROKEN --- appengine_django/__init__.py | 528 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 appengine_django/__init__.py (limited to 'appengine_django/__init__.py') diff --git a/appengine_django/__init__.py b/appengine_django/__init__.py new file mode 100644 index 0000000..629695d --- /dev/null +++ b/appengine_django/__init__.py @@ -0,0 +1,528 @@ +#!/usr/bin/python2.5 +# +# Copyright 2008 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Support for integrating a Django project with the appengine infrastructure. + +This requires Django 1.0beta1 or greater. + +This module enables you to use the Django manage.py utility and *some* of it's +subcommands. View the help of manage.py for exact details. + +Additionally this module takes care of initialising the datastore (and a test +datastore) so that the Django test infrastructure can be used for your +appengine project. + +To use this module add the following two lines to your main.py and manage.py +scripts at the end of your imports: + from appengine_django import InstallAppengineHelperForDjango + InstallAppengineHelperForDjango() + +If you would like to use a version of Django other than that provided by the +system all you need to do is include it in a directory just above this helper, +eg: + appengine_django/__init__.py - This file + django/... - your private copy of Django. +""" + +import logging +import os +import re +import sys +import unittest +import zipfile + + +DIR_PATH = os.path.abspath(os.path.dirname(__file__)) +PARENT_DIR = os.path.dirname(DIR_PATH) +if PARENT_DIR.endswith(".zip"): + # Check for appengine_django itself being in a zipfile. + PARENT_DIR = os.path.dirname(PARENT_DIR) + +# Add this project to the start of sys path to enable direct imports. +sys.path = [PARENT_DIR,] + sys.path + +# Try to import the appengine code from the system path. +try: + from google.appengine.api import apiproxy_stub_map +except ImportError, e: + # Not on the system path. Build a list of alternative paths where it may be. + # First look within the project for a local copy, then look for where the Mac + # OS SDK installs it. + paths = [os.path.join(PARENT_DIR, '.google_appengine'), + os.path.join(PARENT_DIR, 'google_appengine'), + '/usr/local/google_appengine'] + # Then if on windows, look for where the Windows SDK installed it. + for path in os.environ.get('PATH', '').split(';'): + path = path.rstrip('\\') + if path.endswith('google_appengine'): + paths.append(path) + try: + from win32com.shell import shell + from win32com.shell import shellcon + id_list = shell.SHGetSpecialFolderLocation( + 0, shellcon.CSIDL_PROGRAM_FILES) + program_files = shell.SHGetPathFromIDList(id_list) + paths.append(os.path.join(program_files, 'Google', + 'google_appengine')) + except ImportError, e: + # Not windows. + pass + # Loop through all possible paths and look for the SDK dir. + SDK_PATH = None + for sdk_path in paths: + if os.path.exists(sdk_path): + SDK_PATH = os.path.realpath(sdk_path) + break + if SDK_PATH is None: + # The SDK could not be found in any known location. + sys.stderr.write("The Google App Engine SDK could not be found!\n") + sys.stderr.write("See README for installation instructions.\n") + sys.exit(1) + if SDK_PATH == os.path.join(PARENT_DIR, 'google_appengine'): + logging.warn('Loading the SDK from the \'google_appengine\' subdirectory ' + 'is now deprecated!') + logging.warn('Please move the SDK to a subdirectory named ' + '\'.google_appengine\' instead.') + logging.warn('See README for further details.') + # Add the SDK and the libraries within it to the system path. + EXTRA_PATHS = [ + SDK_PATH, + os.path.join(SDK_PATH, 'lib', 'antlr3'), + os.path.join(SDK_PATH, 'lib', 'django'), + os.path.join(SDK_PATH, 'lib', 'webob'), + os.path.join(SDK_PATH, 'lib', 'yaml', 'lib'), + ] + # Add SDK paths at the start of sys.path, but after the local directory which + # was added to the start of sys.path on line 50 above. The local directory + # must come first to allow the local imports to override the SDK and + # site-packages directories. + sys.path = sys.path[0:1] + EXTRA_PATHS + sys.path[1:] + from google.appengine.api import apiproxy_stub_map + +# Look for a zipped copy of Django. +have_django_zip = False +django_zip_path = os.path.join(PARENT_DIR, 'django.zip') +if os.path.exists(django_zip_path): + have_django_zip = True + sys.path.insert(1, django_zip_path) + +# Remove the standard version of Django if a local copy has been provided. +if have_django_zip or os.path.exists(os.path.join(PARENT_DIR, 'django')): + for k in [k for k in sys.modules if k.startswith('django')]: + del sys.modules[k] + +# Must set this env var *before* importing any more of Django. +os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' + +from django import VERSION +from django.conf import settings + +from google.appengine.api import yaml_errors + +# Flags made available this module +appid = None +have_appserver = False + +# Hide everything other than the flags above and the install function. +__all__ = ("appid", "have_appserver", "have_django_zip", + "django_zip_path", "InstallAppengineHelperForDjango") + + +INCOMPATIBLE_COMMANDS = ["adminindex", "createcachetable", "dbshell", + "inspectdb", "runfcgi", "syncdb", "validate"] + + +def LoadAppengineEnvironment(): + """Loads the appengine environment. + + Returns: + This function has no return value, but it sets the following parameters on + this package: + - appid: The name of the application. + - have_appserver: Boolean parameter which is True if the code is being run + from within the appserver environment. + """ + global appid, have_appserver + + # Detect if we are running under an appserver. + have_appserver = False + stub = apiproxy_stub_map.apiproxy.GetStub("datastore_v3") + if stub: + have_appserver = True + + # Load the application identifier. + if have_appserver: + appid = os.environ.get("APPLICATION_ID", "unknown") + else: + # Running as manage.py script, read from config file. + try: + from google.appengine.tools import dev_appserver + appconfig, unused_matcher = dev_appserver.LoadAppConfig(PARENT_DIR, {}) + appid = appconfig.application + except (ImportError, yaml_errors.EventListenerYAMLError), e: + logging.warn("Could not read the Application ID from app.yaml. " + "This may break things in unusual ways!") + # Something went wrong. + appid = "unknown" + + logging.debug("Loading application '%s' %s an appserver" % + (appid, have_appserver and "with" or "without")) + + +def InstallAppengineDatabaseBackend(): + """Installs the appengine database backend into Django. + + The appengine database lives in the db/ subdirectory of this package, but is + known as "appengine" to Django. This function installs the module where + Django expects to find its database backends. + """ + from appengine_django import db + sys.modules['django.db.backends.appengine'] = db + logging.debug("Installed appengine database backend") + + +def InstallGoogleMemcache(): + """Installs the Google memcache into Django. + + By default django tries to import standard memcache module. + Because appengine memcache is API compatible with Python memcache module, + we can trick Django to think it is installed and to use it. + + Now you can use CACHE_BACKEND = 'memcached://' in settings.py. IP address + and port number are not required. + """ + from google.appengine.api import memcache + sys.modules['memcache'] = memcache + logging.debug("Installed App Engine memcache backend") + + +def InstallDjangoModuleReplacements(): + """Replaces internal Django modules with App Engine compatible versions.""" + + # Replace the session module with a partial replacement overlay using + # __path__ so that portions not replaced will fall through to the original + # implementation. + try: + from django.contrib import sessions + orig_path = sessions.__path__[0] + sessions.__path__.insert(0, os.path.join(DIR_PATH, 'sessions')) + from django.contrib.sessions import backends + backends.__path__.append(os.path.join(orig_path, 'backends')) + except ImportError: + logging.debug("No Django session support available") + + # Replace incompatible dispatchers. + import django.core.signals + import django.db + import django.dispatch.dispatcher + + # Rollback occurs automatically on Google App Engine. Disable the Django + # rollback handler. + CheckedException = KeyError + def _disconnectSignal(): + django.core.signals.got_request_exception.disconnect( + django.db._rollback_on_exception) + + try: + _disconnectSignal() + except CheckedException, e: + logging.debug("Django rollback handler appears to be already disabled.") + +def PatchDjangoSerializationModules(): + """Monkey patches the Django serialization modules. + + The standard Django serialization modules to not correctly handle the + datastore models provided by this package. This method installs replacements + for selected modules and methods to give Django the capability to correctly + serialize and deserialize datastore models. + """ + # These can't be imported until InstallAppengineDatabaseBackend has run. + from django.core.serializers import python + from appengine_django.serializer.python import Deserializer + if not hasattr(settings, "SERIALIZATION_MODULES"): + settings.SERIALIZATION_MODULES = {} + base_module = "appengine_django" + settings.SERIALIZATION_MODULES["xml"] = "%s.serializer.xml" % base_module + python.Deserializer = Deserializer + PatchDeserializedObjectClass() + DisableModelValidation() + logging.debug("Installed appengine json and python serialization modules") + + +def PatchDeserializedObjectClass(): + """Patches the DeserializedObject class. + + The default implementation calls save directly on the django Model base + class to avoid pre-save handlers. The model class provided by this package + is not derived from the Django Model class and therefore must be called + directly. + + Additionally we need to clear the internal _parent attribute as it may + contain a FakeParent class that is used to deserialize instances without + needing to load the parent instance itself. See the PythonDeserializer for + more details. + """ + # This can't be imported until InstallAppengineDatabaseBackend has run. + from django.core.serializers import base + class NewDeserializedObject(base.DeserializedObject): + def save(self, save_m2m=True): + self.object.save() + self.object._parent = None + base.DeserializedObject = NewDeserializedObject + logging.debug("Replacement DeserializedObject class installed") + +def DisableModelValidation(): + """Disables Django's model validation routines. + + The model validation is primarily concerned with validating foreign key + references. There is no equivalent checking code for datastore References at + this time. + + Validation needs to be disabled or serialization/deserialization will fail. + """ + from django.core.management import validation + validation.get_validation_errors = lambda x, y=0: 0 + logging.debug("Django SQL model validation disabled") + +def CleanupDjangoSettings(): + """Removes incompatible entries from the django settings module.""" + + # Ensure this module is installed as an application. + apps = getattr(settings, "INSTALLED_APPS", ()) + found = False + for app in apps: + if app.endswith("appengine_django"): + found = True + break + if not found: + logging.warn("appengine_django module is not listed as an application!") + apps += ("appengine_django",) + setattr(settings, "INSTALLED_APPS", apps) + logging.info("Added 'appengine_django' as an application") + + # Ensure the database backend is appropriately configured. + dbe = getattr(settings, "DATABASE_ENGINE", "") + if dbe != "appengine": + settings.DATABASE_ENGINE = "appengine" + logging.warn("DATABASE_ENGINE is not configured as 'appengine'. " + "Value overriden!") + for var in ["NAME", "USER", "PASSWORD", "HOST", "PORT"]: + val = getattr(settings, "DATABASE_%s" % var, "") + if val: + setattr(settings, "DATABASE_%s" % var, "") + logging.warn("DATABASE_%s should be blank. Value overriden!") + + # Remove incompatible middleware modules. + mw_mods = list(getattr(settings, "MIDDLEWARE_CLASSES", ())) + disallowed_middleware_mods = ( + 'django.middleware.doc.XViewMiddleware',) + for modname in mw_mods[:]: + if modname in disallowed_middleware_mods: + # Currently only the CommonMiddleware has been ported. As other base + # modules are converted, remove from the disallowed_middleware_mods + # tuple. + mw_mods.remove(modname) + logging.warn("Middleware module '%s' is not compatible. Removed!" % + modname) + setattr(settings, "MIDDLEWARE_CLASSES", tuple(mw_mods)) + + # Remove incompatible application modules + app_mods = list(getattr(settings, "INSTALLED_APPS", ())) + disallowed_apps = ( + 'django.contrib.contenttypes', + 'django.contrib.sites',) + for app in app_mods[:]: + if app in disallowed_apps: + app_mods.remove(app) + logging.warn("Application module '%s' is not compatible. Removed!" % app) + setattr(settings, "INSTALLED_APPS", tuple(app_mods)) + + # Remove incompatible session backends. + session_backend = getattr(settings, "SESSION_ENGINE", "") + if session_backend.endswith("file"): + logging.warn("File session backend is not compatible. Overriden " + "to use db backend!") + setattr(settings, "SESSION_ENGINE", "django.contrib.sessions.backends.db") + + +def ModifyAvailableCommands(): + """Removes incompatible commands and installs replacements where possible.""" + if have_appserver: + # Commands are not used when running from an appserver. + return + from django.core import management + project_directory = os.path.join(__path__[0], "../") + if have_django_zip: + FindCommandsInZipfile.orig = management.find_commands + management.find_commands = FindCommandsInZipfile + management.get_commands() + # Replace startapp command which is set by previous call to get_commands(). + from appengine_django.management.commands.startapp import ProjectCommand + management._commands['startapp'] = ProjectCommand(project_directory) + RemoveCommands(management._commands) + logging.debug("Removed incompatible Django manage.py commands") + + +def FindCommandsInZipfile(management_dir): + """ + Given a path to a management directory, returns a list of all the command + names that are available. + + This implementation also works when Django is loaded from a zip. + + Returns an empty list if no commands are defined. + """ + zip_marker = ".zip%s" % os.sep + if zip_marker not in management_dir: + return FindCommandsInZipfile.orig(management_dir) + + # Django is sourced from a zipfile, ask zip module for a list of files. + filename, path = management_dir.split(zip_marker) + zipinfo = zipfile.ZipFile("%s.zip" % filename) + + # Add commands directory to management path. + path = os.path.join(path, "commands") + + # The zipfile module returns paths in the format of the operating system + # that created the zipfile! This may not match the path to the zipfile + # itself. Convert operating system specific characters to a standard + # character (#) to compare paths to work around this. + path_normalise = re.compile(r"[/\\]") + path = path_normalise.sub("#", path) + def _IsCmd(t): + """Returns true if t matches the criteria for a command module.""" + filename = os.path.basename(t) + t = path_normalise.sub("#", t) + if not t.startswith(path): + return False + if filename.startswith("_") or not t.endswith(".py"): + return False + return True + + return [os.path.basename(f)[:-3] for f in zipinfo.namelist() if _IsCmd(f)] + + +def RemoveCommands(command_dict): + """Removes incompatible commands from the specified command dictionary.""" + for cmd in command_dict.keys(): + if cmd.startswith("sql"): + del command_dict[cmd] + elif cmd in INCOMPATIBLE_COMMANDS: + del command_dict[cmd] + + +def InstallReplacementImpModule(): + """Install a replacement for the imp module removed by the appserver. + + This is only to find mangement modules provided by applications. + """ + if not have_appserver: + return + modname = 'appengine_django.replacement_imp' + imp_mod = __import__(modname, {}, [], ['']) + sys.modules['imp'] = imp_mod + logging.debug("Installed replacement imp module") + + +def InstallAppengineHelperForDjango(): + """Installs and Patches all of the classes/methods required for integration. + + If the variable DEBUG_APPENGINE_DJANGO is set in the environment verbose + logging of the actions taken will be enabled. + """ + if VERSION < (1, 0, None): + logging.error("Django 1.0 or greater is required!") + sys.exit(1) + + if os.getenv("DEBUG_APPENGINE_DJANGO"): + logging.getLogger().setLevel(logging.DEBUG) + else: + logging.getLogger().setLevel(logging.INFO) + logging.debug("Loading the Google App Engine Helper for Django...") + + # Force Django to reload its settings. + settings._target = None + + LoadAppengineEnvironment() + InstallReplacementImpModule() + InstallAppengineDatabaseBackend() + InstallModelForm() + InstallGoogleMemcache() + InstallDjangoModuleReplacements() + PatchDjangoSerializationModules() + CleanupDjangoSettings() + ModifyAvailableCommands() + InstallGoogleSMTPConnection() + InstallAuthentication() + + logging.debug("Successfully loaded the Google App Engine Helper for Django.") + + +def InstallGoogleSMTPConnection(): + from appengine_django import mail as gmail + from django.core import mail + logging.debug("Installing Google Email Adapter for Django") + mail.SMTPConnection = gmail.GoogleSMTPConnection + mail.mail_admins = gmail.mail_admins + mail.mail_managers = gmail.mail_managers + + +def InstallAuthentication(): + if "django.contrib.auth" not in settings.INSTALLED_APPS: + return + try: + from appengine_django.auth import models as helper_models + from django.contrib.auth import models + models.User = helper_models.User + models.Group = helper_models.Group + models.Permission = helper_models.Permission + models.Message = helper_models.Message + from django.contrib.auth import middleware as django_middleware + from appengine_django.auth.middleware import AuthenticationMiddleware + django_middleware.AuthenticationMiddleware = AuthenticationMiddleware + from django.contrib.auth import decorators as django_decorators + from appengine_django.auth.decorators import login_required + django_decorators.login_required = login_required + from django.contrib import auth as django_auth + from django.contrib.auth import tests as django_tests + django_auth.suite = unittest.TestSuite + django_tests.suite = unittest.TestSuite + logging.debug("Installing authentication framework") + except ImportError: + logging.debug("No Django authentication support available") + + +def InstallModelForm(): + """Replace Django ModelForm with the AppEngine ModelForm.""" + # This MUST happen as early as possible, but after any auth model patching. + from google.appengine.ext.db import djangoforms as aeforms + try: + # pre 1.0 + from django import newforms as forms + except ImportError: + from django import forms + + forms.ModelForm = aeforms.ModelForm + + # Extend ModelForm with support for EmailProperty + # TODO: This should be submitted to the main App Engine SDK. + from google.appengine.ext.db import EmailProperty + def get_form_field(self, **kwargs): + """Return a Django form field appropriate for an email property.""" + defaults = {'form_class': forms.EmailField} + defaults.update(kwargs) + return super(EmailProperty, self).get_form_field(**defaults) + EmailProperty.get_form_field = get_form_field -- cgit v1.2.3