Skip to content

Appengine 1.4 #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 66 commits into
base: appengine-1.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
fbcecf5
git ignore
aburgel Nov 26, 2011
f265731
appengine key and ancestor support
aburgel Nov 26, 2011
decfc12
fixes for non-primary GAEKeys
aburgel Nov 26, 2011
87a9f85
use id_or_name as string value
aburgel Nov 29, 2011
fd234e6
better value conversion to match str from GAEKey
aburgel Nov 29, 2011
150c915
test cases for different kinds of pk representations
aburgel Nov 29, 2011
29a1c62
fix cmp for GAEKey
aburgel Dec 6, 2011
1a1456a
merge upstream/develop
aburgel Dec 27, 2011
091b12c
use methods instead of properties for GAEKey fields
aburgel Dec 29, 2011
b5f126a
Merge branch 'develop' of git://github.com/django-nonrel/djangoappeng…
aburgel Dec 29, 2011
902c363
remove missed @property, update unit tests
aburgel Dec 29, 2011
6aa66b7
use real_key for serialization
aburgel Jan 25, 2012
216aa48
modify SqlInsertCompiler to handle bulk inserts
aburgel Mar 17, 2012
7df916e
remove usage of XMLField
aburgel Mar 17, 2012
8208067
repr method for GAEKey
aburgel Mar 17, 2012
2fc14a5
do not encode cursor if it doesnt exist
aburgel Mar 17, 2012
b23cc69
do not allow null primary keys, check for empty strings when creating…
aburgel Mar 18, 2012
cc73be7
merge develop branch
aburgel Mar 23, 2012
60e62a7
merge develop branch
aburgel Mar 23, 2012
6a10a9d
rewrite ancestor queries for type-conversion-refactor
aburgel Mar 23, 2012
d38742e
django 1.4 updates
aburgel Mar 23, 2012
7407854
DBKeyField is a special case for conversion
aburgel Apr 20, 2012
b77c183
add missing import
aburgel Apr 20, 2012
9be9b5f
dbkeyfield should not be forced to nullable
aburgel Apr 20, 2012
4c6ebdf
added make_key function to simplify created DbKeys from models
aburgel Apr 20, 2012
32d2e17
Merge commit '257e3390ab4768abbb6d82af6c16438cb9ede3cb' into django-1.4
May 22, 2012
13c8b64
Merge branch 'feature/ancestor-query-1.4' of git://github.com/django-…
May 23, 2012
a417ac0
Add some syntactic sugar classes for doing ancestor queries.
May 25, 2012
e900212
Put datastore persistence patch back in, it's still necessary for 1.6.5
May 26, 2012
dd03270
Also fix db/utils.py so it works in python 2.5
May 30, 2012
b38cb39
db_type may be marked "date"/"time", as long as it's derived from Dat…
Jun 29, 2012
9c84c29
Merge commit 'b38cb39733b6b142c130a081f85503b8c1160d01' into django-1.4
Jun 29, 2012
f592f4e
Add dev_appserver compatible version of LiveServerTestCase.
Jul 13, 2012
8abaf2a
Hack around two threading issues with the LiveServerTestCase
Jul 14, 2012
637d8e1
Make logout url work for LiveServerTest
Jul 16, 2012
05afb8c
Switch to WSGI for python 2.7
invalid-email-address Aug 30, 2012
9f271c2
Add synchronization between the LiveServerTestCase and LiveServerThread.
invalid-email-address Oct 4, 2012
9917952
Merge branch 'features/django-1.4' of git://github.com/django-nonrel/…
invalid-email-address Oct 4, 2012
e32ed1c
Clean up comments in test utilities.
invalid-email-address Oct 4, 2012
69cfd34
Add input reader utility classes for use with the mapreduce API.
invalid-email-address Oct 4, 2012
eb29ef3
Fix sync error between threads when loading fixtures.
invalid-email-address Oct 12, 2012
3139a05
A bunch of hacks to get django-nonrel serving on devappserver2.
Feb 8, 2013
c2293fe
Fix a bug that broke backwards compatibility with 1.7.4
Feb 9, 2013
6e94cef
Importing Testbed() on production throws some SSL warnings in the log.
Feb 15, 2013
9487b33
Manually rebase my own working branch on top of upstream features/dja…
Feb 18, 2013
5eab9de
Add support for devappserver2.
Feb 18, 2013
6bca0c5
Make 'python manage.py runserver' commands work with either GAE SDK 1…
Mar 12, 2013
6a472a8
Add url property to BlobStoreFile and BlobstoreUploadedFile.
Mar 16, 2013
ff107ca
Patch manage.py test to enable file uploads to blobstore.
Mar 19, 2013
0e49f3f
Streamline the last checkin a bit by automatically pulling up the blo…
Mar 19, 2013
ec08bb8
Add testing of File fields that store BlobstoreFiles
Mar 19, 2013
fdc3e17
Update to work on SDK 1.7.6
Mar 19, 2013
2a09904
Make manage.py shell work for datastore accesses on devappserver2
Mar 20, 2013
4993104
Add support for devappserver2 to LiveServerTestCase.
Mar 20, 2013
29ae1a1
Clean up breakpoint.
Mar 20, 2013
ba8122b
Add support for auto_id_policy to the command line.
Mar 22, 2013
d607801
Make sure we set up datastore stubs properly in stubs.py, this makes …
Mar 23, 2013
64c4e64
Revert changes to utils.py that were needed for the standalone devapp…
Mar 25, 2013
9f059e7
Add __str__ and __unicode___ methods to BlobstoreFile so filenames ar…
Aug 7, 2013
297e10c
Merge branch 'appengine-1.4' of github.com:django-nonrel/djangoappeng…
Aug 8, 2013
7ed1b53
Force dev_appserver datastore consistency when calling loaddata.
Aug 19, 2013
122b1fe
Fix for last change not working properly on the original dev_appserve…
Aug 21, 2013
ca5cd85
Update input readers for reading keys and raw entities given Django m…
Oct 12, 2013
a7a4426
Patch tests so they don't throw an exception when calling the latest …
Oct 30, 2013
85f5f15
Attempt to fix django-nonrel Travis-CI test by disabling images stub.
Oct 30, 2013
0376e47
Reinstating the init_images_stub, since it's required for the blobsto…
Oct 30, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 55 additions & 34 deletions djangoappengine/boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
import os
import sys

global devappserver_ver
try:
import force_devappserver
devappserver_ver = force_devappserver.devappserver_ver
except:
devappserver_ver = 1

def find_project_dir():
"""
Go through the path, and look for manage.py
Expand Down Expand Up @@ -70,6 +77,10 @@ def setup_env():

# Then call fix_sys_path from the SDK
from dev_appserver import fix_sys_path
if devappserver_ver == 2:
# emulate dev_appserver._run_file in devappserver2
from dev_appserver import _SYS_PATH_ADDITIONS
sys.path = _SYS_PATH_ADDITIONS['_python_runtime.py'] + sys.path
fix_sys_path()

setup_project()
Expand Down Expand Up @@ -146,41 +157,50 @@ def setup_project():
# enable https connections (seem to be broken on Windows because
# the _ssl module is disallowed).
if not have_appserver:
from google.appengine.tools import dev_appserver
try:
# Backup os.environ. It gets overwritten by the
# dev_appserver, but it's needed by the subprocess module.
env = dev_appserver.DEFAULT_ENV
dev_appserver.DEFAULT_ENV = os.environ.copy()
dev_appserver.DEFAULT_ENV.update(env)
# Backup the buffer() builtin. The subprocess in Python 2.5
# on Linux and OS X uses needs it, but the dev_appserver
# removes it.
dev_appserver.buffer = buffer
except AttributeError:
logging.warn("Could not patch the default environment. "
"The subprocess module will not work correctly.")

try:
# Allow importing compiler/parser, _ssl (for https),
# _io for Python 2.7 io support on OS X
dev_appserver.HardenedModulesHook._WHITE_LIST_C_MODULES.extend(
('parser', '_ssl', '_io'))
except AttributeError:
logging.warn("Could not patch modules whitelist. the compiler "
"and parser modules will not work and SSL support "
"is disabled.")
elif not on_production_server:
try:
# Restore the real subprocess module.
from google.appengine.api.mail_stub import subprocess
sys.modules['subprocess'] = subprocess
# Re-inject the buffer() builtin into the subprocess module.
if devappserver_ver == 1:
from google.appengine.tools import dev_appserver
subprocess.buffer = dev_appserver.buffer
except Exception, e:
logging.warn("Could not add the subprocess module to the "
"sandbox: %s" % e)
try:
# Backup os.environ. It gets overwritten by the
# dev_appserver, but it's needed by the subprocess module.
env = dev_appserver.DEFAULT_ENV
dev_appserver.DEFAULT_ENV = os.environ.copy()
dev_appserver.DEFAULT_ENV.update(env)
# Backup the buffer() builtin. The subprocess in Python 2.5
# on Linux and OS X uses needs it, but the dev_appserver
# removes it.
dev_appserver.buffer = buffer
except AttributeError:
logging.warn("Could not patch the default environment. "
"The subprocess module will not work correctly.")

try:
# Allow importing compiler/parser, _ssl (for https),
# _io for Python 2.7 io support on OS X
dev_appserver.HardenedModulesHook._WHITE_LIST_C_MODULES.extend(
('parser', '_ssl', '_io'))
except AttributeError:
logging.warn("Could not patch modules whitelist. the compiler "
"and parser modules will not work and SSL support "
"is disabled.")
# In SDK 1.6.4, the datastore doesn't save automatically on exit.
# Register a handler to make sure we save. This is important on
# manage.py commands other than 'runserver'. Note that with runserver,
# the datastore is flushed twice. This should be acceptable.
import atexit
if hasattr(dev_appserver, 'TearDownStubs'):
atexit.register(dev_appserver.TearDownStubs)
elif not on_production_server:
if devappserver_ver == 1:
try:
# Restore the real subprocess module.
from google.appengine.tools import dev_appserver
from google.appengine.api.mail_stub import subprocess
sys.modules['subprocess'] = subprocess
# Re-inject the buffer() builtin into the subprocess module.
subprocess.buffer = dev_appserver.buffer
except Exception, e:
logging.warn("Could not add the subprocess module to the "
"sandbox: %s" % e)

os.environ.update(env_ext)

Expand All @@ -202,3 +222,4 @@ def setup_project():
while path in sys.path:
sys.path.remove(path)
sys.path = extra_paths + sys.path

11 changes: 6 additions & 5 deletions djangoappengine/db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ def _value_for_db(self, value, field, field_kind, db_type, lookup):
if db_type == 'key':
# value = self._value_for_db_key(value, field_kind)
try:
value = key_from_path(field.model._meta.db_table, value)
if not isinstance(value, Key):
value = key_from_path(field.model._meta.db_table, value)
except (BadArgumentError, BadValueError,):
raise DatabaseError("Only strings and positive integers "
"may be used as keys on GAE.")
Expand All @@ -169,9 +170,9 @@ def _value_for_db(self, value, field, field_kind, db_type, lookup):

# Store all date / time values as datetimes, by using some
# default time or date.
elif db_type == 'date':
elif db_type == 'date' and isinstance(value, datetime.date):
value = datetime.datetime.combine(value, self.DEFAULT_TIME)
elif db_type == 'time':
elif db_type == 'time' and isinstance(value, datetime.time):
value = datetime.datetime.combine(self.DEFAULT_DATE, value)

# Store BlobField, DictField and EmbeddedModelField values as Blobs.
Expand Down Expand Up @@ -207,9 +208,9 @@ def _value_from_db(self, value, field, field_kind, db_type):
value = unicode(value)

# Dates and times are stored as datetimes, drop the added part.
elif db_type == 'date':
elif db_type == 'date' and isinstance(value, datetime.datetime):
value = value.date()
elif db_type == 'time':
elif db_type == 'time' and isinstance(value, datetime.datetime):
value = value.time()

# Convert GAE Blobs to plain strings for Django.
Expand Down
14 changes: 13 additions & 1 deletion djangoappengine/db/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from .db_settings import get_model_indexes
from .expressions import ExpressionEvaluator
from .utils import commit_locked
from .utils import AncestorKey, commit_locked


# Valid query types (a dictionary is used for speedy lookups).
Expand Down Expand Up @@ -87,6 +87,7 @@ def __init__(self, compiler, fields):
self.included_pks = None
self.excluded_pks = ()
self.has_negated_exact_filter = False
self.ancestor_key = None
self.ordering = []
self.db_table = self.query.get_meta().db_table
self.pks_only = (len(fields) == 1 and fields[0].primary_key)
Expand Down Expand Up @@ -197,6 +198,14 @@ def add_filter(self, field, lookup_type, negated, value):
# Optimization: batch-get by key; this is only suitable for
# primary keys, not for anything that uses the key type.
if field.primary_key and lookup_type in ('exact', 'in'):
if lookup_type == 'exact' and isinstance(value, AncestorKey):
if negated:
raise DatabaseError("You can't negate an ancestor operator.")
if self.ancestor_key is not None:
raise DatabaseError("You can't use more than one ancestor operator.")
self.ancestor_key = value.key
return

if self.included_pks is not None:
raise DatabaseError("You can't apply multiple AND "
"filters on the primary key. "
Expand Down Expand Up @@ -318,6 +327,8 @@ def _make_entity(self, entity):
def _build_query(self):
for query in self.gae_query:
query.Order(*self.ordering)
if self.ancestor_key:
query.Ancestor(self.ancestor_key)
if len(self.gae_query) > 1:
return MultiQuery(self.gae_query, self.ordering)
return self.gae_query[0]
Expand Down Expand Up @@ -403,6 +414,7 @@ def insert(self, data_list, return_id=False):
if value is not None:
kwds['id'] = value.id()
kwds['name'] = value.name()
kwds['parent'] = value.parent()

# GAE does not store empty lists (and even does not allow
# passing empty lists to Entity.update) so skip them.
Expand Down
7 changes: 7 additions & 0 deletions djangoappengine/db/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ def db_type(self, field):
field is to be indexed, and the "text" db_type (db.Text) if
it's registered as unindexed.
"""
from djangoappengine.fields import DbKeyField

# DBKeyField reads/stores db.Key objects directly
# so its treated as a special case
if isinstance(field, DbKeyField):
return field.db_type(connection=self.connection)

if self.connection.settings_dict.get('STORE_RELATIONS_AS_DB_KEYS'):
if field.primary_key or field.rel is not None:
return 'key'
Expand Down
Empty file.
14 changes: 14 additions & 0 deletions djangoappengine/db/models/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.db.models import Manager as _baseManager
from djangoappengine.db.utils import as_ancestor
from djangoappengine.db.models.query import QuerySet

class Manager(_baseManager):

def get_query_set(self):
"""Returns a new QuerySet object. Subclasses can override this method
to easily customize the behavior of the Manager.
"""
return QuerySet(self.model, using=self._db)

def ancestor(self, ancestor):
return self.get_query_set().ancestor(ancestor)
17 changes: 17 additions & 0 deletions djangoappengine/db/models/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.db.models.query import QuerySet as _baseQuerySet
from djangoappengine.db.utils import as_ancestor

class QuerySet(_baseQuerySet):
def ancestor(self, ancestor):
"""
Returns a new QuerySet instance with the args ANDed to the existing
set.
"""
return self._filter_or_exclude(False, pk=as_ancestor(ancestor))

class EmptyQuerySet(QuerySet):
def ancestor(self, *args, **kwargs):
"""
Always returns EmptyQuerySet.
"""
return self
18 changes: 15 additions & 3 deletions djangoappengine/db/stubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import time
from urllib2 import HTTPError, URLError

from ..boot import PROJECT_DIR
from ..boot import PROJECT_DIR, devappserver_ver
from ..utils import appid, have_appserver


Expand Down Expand Up @@ -62,9 +62,13 @@ def activate_test_stubs(self, connection):
self.testbed.init_memcache_stub()
self.testbed.init_taskqueue_stub(auto_task_running=True, root_path=PROJECT_DIR)
self.testbed.init_urlfetch_stub()
self.testbed.init_user_stub()
self.testbed.init_user_stub(True, **{ 'logout_url' : '/_ah/login?continue=%s&action=Logout',
'login_url' : '/_ah/login?continue=%s' })
self.testbed.init_xmpp_stub()
self.testbed.init_channel_stub()
self.testbed.init_files_stub(True)
self.testbed.init_blobstore_stub(True)
self.testbed.init_images_stub(True)

def deactivate_test_stubs(self):
if self.active_stubs == 'test':
Expand All @@ -82,9 +86,17 @@ def setup_local_stubs(self, connection):
log_level = logging.getLogger().getEffectiveLevel()
logging.getLogger().setLevel(logging.WARNING)
from google.appengine.tools import dev_appserver
dev_appserver.SetupStubs('dev~' + appid, **args)
dev_appserver.SetupStubs('dev~' + appid,
_use_atexit_for_datastore_stub=True,
**args)
logging.getLogger().setLevel(log_level)
self.active_stubs = 'local'
if devappserver_ver == 2:
# Mimic google.appengine.tools.devappserver2
os.environ['TZ'] = 'UTC'
if hasattr(time, 'tzset'):
# time.tzet() should be called on Unix, but doesn't exist on Windows.
time.tzset()

def setup_remote_stubs(self, connection):
if self.active_stubs == 'remote':
Expand Down
40 changes: 38 additions & 2 deletions djangoappengine/db/utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from django.db import DEFAULT_DB_ALIAS

from google.appengine.api.datastore import Key
from google.appengine.datastore.datastore_query import Cursor
from django.db import models, DEFAULT_DB_ALIAS

try:
from functools import wraps
except ImportError:
from django.utils.functional import wraps # Python 2.3, 2.4 fallback.


class CursorQueryMixin(object):

def clone(self, *args, **kwargs):
kwargs['_gae_cursor'] = getattr(self, '_gae_cursor', None)
kwargs['_gae_start_cursor'] = getattr(self, '_gae_start_cursor', None)
kwargs['_gae_end_cursor'] = getattr(self, '_gae_end_cursor', None)
kwargs['_gae_config'] = getattr(self, '_gae_config', None)
Expand Down Expand Up @@ -90,3 +93,36 @@ def _commit_locked(*args, **kw):
if callable(func_or_using):
return inner_commit_locked(func_or_using, DEFAULT_DB_ALIAS)
return lambda func: inner_commit_locked(func, func_or_using)

class AncestorKey(object):
def __init__(self, key):
self.key = key

def as_ancestor(key_or_model):
if key_or_model is None:
raise ValueError("key_or_model must not be None")

if isinstance(key_or_model, models.Model):
key_or_model = Key.from_path(key_or_model._meta.db_table, key_or_model.pk)

return AncestorKey(key_or_model)

def make_key(*args, **kwargs):
parent = kwargs.pop('parent', None)

if kwargs:
raise AssertionError('Excess keyword arguments; received %s' % kwargs)

if not args or len(args) % 2:
raise AssertionError('A non-zero even number of positional arguments is required; received %s' % args)

if isinstance(parent, models.Model):
parent = Key.from_path(parent._meta.db_table, parent.pk)

converted_args = []
for i in xrange(0, len(args), 2):
model, id_or_name = args[i:i+2]
converted_args.extend((model._meta.db_table, id_or_name))

newkwargs = { 'parent' : parent }
return Key.from_path(*converted_args, **newkwargs)
Loading