def test_key_inequality(self): ''' Make sure keys don't equal each other when they shouldn't. ''' conditions = [] # test with determininstic ID k1 = model.Key("TestKind", "blabs") k2 = model.Key("TestKind", "blobs") conditions.append((k1 != k2)) # test kinded keys k3 = model.Key("TestKind") k4 = model.Key("TestOtherKind") conditions.append((k3 != k4)) # test ancestored keys with different IDs k5 = model.Key("TestSubkind", "blabs", parent=k1) k6 = model.Key("TestSubkind", "blobs", parent=k1) conditions.append((k5 != k6)) # test ancestored keys with different kinds k7 = model.Key("TestSubkindOne", "blabs", parent=k1) k8 = model.Key("TestSubkindTwo", "blabs", parent=k1) conditions.append((k7 != k8)) # test ancestored keys with different parents k9 = model.Key("TestSubkind", "blabs", parent=k1) k10 = model.Key("TestSubkind", "blabs", parent=k2) conditions.append((k9 != k10)) [self.assertTrue(condition) for condition in conditions]
def test_key_construct_multiple_formats(self): ''' Test constuction of a `Key` with multiple formats, which is not supported. ''' # sample key ok = model.Key("Sample", "sample_id") # try and make a key with multiple formats with self.assertRaises(TypeError): model.Key(raw=ok.flatten(False)[1], urlsafe=ok.urlsafe())
def test_key_with_overflowing_schema(self): ''' Test construction of a `Key` with too many schema items. ''' # try and make a key with a ton of arguments with self.assertRaises(TypeError): model.Key("SampleKind", "id", "coolstring", "whatdowedo", "whenwehave", "thismanyarguments")
def test_model_setkey(self): ''' Test the protected method `_set_key`, which is used by Model API internals. ''' # sample person p = Person(firstname='John') # try writing an invalid key with self.assertRaises(TypeError): p._set_key(5.5) # try writing via kwargs k = model.Key(Person, "john") # try constructing via urlsafe p._set_key(urlsafe=k.urlsafe()) # try constructing via raw p._set_key(raw=k.flatten(False)[1]) # try already-constructed via kwargs p._set_key(constructed=k) # try providing both a value and formats, which should fail with self.assertRaises(TypeError): p._set_key(k, urlsafe=k.urlsafe()) # try providing multiple formats, which should fail with self.assertRaises(TypeError): p._set_key(urlsafe=k.urlsafe(), raw=k.flatten(False)[1]) # try passing nothing, which should fail with self.assertRaises(TypeError): p._set_key(None)
def test_key_overwrite_known_attribute(self): ''' Try overwriting a known (schema-d) attribute. ''' k = model.Key("CoolKind", "coolid") with self.assertRaises(AttributeError): k.kind = "MyKind" with self.assertRaises(AttributeError): k.id = 10
def test_key_with_model_class_kind(self): ''' Test making a `Key` via using a model class as the kind. ''' ## KindedModel # Used to test using classes for kinds in `model.Key`. class KindedModel(model.Model): ''' Sample for testing key creation from model classes. ''' string = basestring # make keys k1 = model.Key("KindedModel", "test_id") k2 = model.Key(KindedModel, "test_id") ko = model.Key(KindedModel) # test keys self.assertEqual(k1.kind, "KindedModel") self.assertEqual(k1.id, "test_id") self.assertEqual(k2.kind, k1.kind) self.assertEqual(ko.kind, k1.kind) self.assertEqual(k2.id, k2.id)
def test_key_ancestry(self): ''' Make a key with ancestry and test it a bunch. ''' # manufacture keys pk = model.Key("ParentKind", "parent_id") ck = model.Key("ChildKind", "child_id", parent=pk) gk = model.Key("GrandchildKind", "grandchild_id", parent=ck) ggk = model.Key("GreatGrandchildKind", "great_grandchild_id", parent=gk) # for each key, make sure parent is set self.assertEqual(pk.parent, None) self.assertEqual(ck.parent, pk) self.assertEqual(gk.parent, ck) self.assertEqual(ggk.parent, gk) # for each key, make sure ancestry works pk_ancestry = [i for i in pk.ancestry] ck_ancestry = [i for i in ck.ancestry] gk_ancestry = [i for i in gk.ancestry] ggk_ancestry = [i for i in ggk.ancestry] # test ancestry paths self.assertEqual(len(pk_ancestry), 1) self.assertEqual(len(ck_ancestry), 2) self.assertEqual(len(gk_ancestry), 3) self.assertEqual(len(ggk_ancestry), 4) # len of a key should always be 1 unless it has ancestry, then it's the length of the ancestry chain self.assertEqual(len(pk), 1) self.assertEqual(len(ck), 2) self.assertEqual(len(gk), 3) self.assertEqual(len(ggk), 4) # ... however all keys should test nonzero-ness (all keys should be nonzero) for k in (pk, ck, gk, ggk): self.assertTrue(k)
def test_key_format(self): ''' Make sure there's a proper format spec on `model.Key`. ''' # build object k1 = model.Key("Test", "testkey") # test class self.assertTrue(hasattr(model.Key, '__schema__')) self.assertIsInstance(model.Key.__schema__, tuple) self.assertTrue((len(model.Key.__schema__) > 1)) # test object self.assertTrue(hasattr(k1, '__schema__')) self.assertIsInstance(model.Key.__schema__, tuple) self.assertTrue((len(model.Key.__schema__) > 1))
def test_key_adapter(self): ''' Make sure the adapter is attached correctly to `model.Key`. ''' # build test obj k = model.Key("TestKind", "test") # make sure an adapter is attached at the class level self.assertTrue(hasattr(model.Key, '__adapter__')) self.assertIsInstance(model.Key.__adapter__, model.adapter.ModelAdapter) # make sure the same is true at the object level self.assertTrue(hasattr(k, '__adapter__')) self.assertIsInstance(model.Key.__adapter__, model.adapter.ModelAdapter)
def factory(cls, parent=None, id=None, **kwargs): ''' ''' # figure out key ID and parent id, parent = ( parent if (not id or (isinstance(basestring, parent) and not parent)) else id, parent if not isinstance(parent, basestring) else None ) # inject ID if we have one if hasattr(cls, 'id'): kwargs['id'] = id # factory object return cls(key=model.Key(cls, id, parent=parent), **kwargs)
def test_model_setvalue(self): ''' Test the protected method `_set_value`, which is used by Model API internals. ''' # sample person p = Person(firstname='John') # try writing a new key x = p._set_value('key', model.Key(Person, "john")) self.assertEqual(x, p) # try writing to invalid property with self.assertRaises(AttributeError): p._set_value('invalidproperty', 'value') # quick test via descriptor API, which should also raise `AttributeError` with self.assertRaises(AttributeError): Person.__dict__['firstname'].__set__(None, 'invalid')
def test_key_auto_id(self): ''' Test an integer-based ID field. ''' class AutoincrementTest(model.Model): ''' Test that keys autoincrement properly when not assigned deterministic name values. ''' message = basestring, {'default': 'Hello, world!'} # put deterministic key a = AutoincrementTest( key=model.Key(AutoincrementTest.kind(), 'testing-string-key')) dk = a.put() # put nondeterministic key #1 nk = AutoincrementTest().put() # put nondeterministic key #2 nk2 = AutoincrementTest().put() self.assertIsInstance(nk.id, int) # nondeterministic should be an int self.assertIsInstance(dk.id, basestring) # deterministic should be a string self.assertTrue((nk2.id > nk.id)) # should be greater
def test_key_equality(self): ''' Make sure keys equal each other when they should. ''' conditions = [] # test with determininstic ID k1 = model.Key("TestKind", "blabs") k2 = model.Key("TestKind", "blabs") conditions.append((k1 == k2)) # test kinded keys k3 = model.Key("TestKind") k4 = model.Key("TestKind") conditions.append((k3 == k4)) # test ancestored keys k5 = model.Key("TestSubkind", "blobs", parent=k1) k6 = model.Key("TestSubkind", "blobs", parent=k2) conditions.append((k5 == k6)) [self.assertTrue(condition) for condition in conditions]
def test_key_set_unknown_attribute(self): ''' Try setting an unknown and known attribute. ''' k = model.Key("CoolKind", "coolid") with self.assertRaises(AttributeError): k.blabble = True # try writing unknown attribute
def make_key(id=None, model=UserSession): ''' ''' return models.Key( model, id or Session.generate_token(Session.config.get('salt', '')))
def execute_query(cls, kind, spec, options, **kwargs): # pragma: no cover ''' Execute a :py:class:`model.Query` across one (or multiple) indexed properties. :param kind: Kind name (``str``) for which we are querying across, or ``None`` if this is a ``kindless`` query. :param spec: Tupled pair of ``filter`` and ``sort`` directives to apply to this :py:class:`Query` (:py:class:`query.Filter` and :py:class:`query.Sort` and their descendents, respectively), like ``(<filters>, <sorts>)``. :param options: Object descendent from, or directly instantiated as :py:class:`QueryOptions`, specifying options for the execution of this :py:class:`Query`. :param **kwargs: Low-level options for handling this query, such as ``pipeline`` (for pipelining support) and ``execute`` (to trigger a buffer flush for a generated or constructed pipeline or operation buffer). :returns: Iterable (``list``) of matching :py:class:`model.Key` yielded by execution of the current :py:class:`Query`. Returns empty iterable (``[]``) in the case that no results could be found. ''' # @TODO(sgammon): desparately needs rewriting. absolute utter plebbery. from canteen import model from canteen.model import query if not kind: # @TODO(sgammon): implement kindless queries raise NotImplementedError( 'Kindless queries are not yet implemented in `Redis`.') # extract filter and sort directives and build ancestor filters, sorts = spec ancestry_parent = model.Key.from_urlsafe( options.ancestor) if options.ancestor else None _data_frame = [] # allocate results window if filters: kinded_key = model.Key(kind, parent=ancestry_parent) ## HUGE HACK: use generate_indexes to make index names. # fix this plz _filters, _filter_i_lookup = {}, set() for _f in filters: origin, meta, property_map = cls.generate_indexes( kinded_key, {_f.target.name: (_f.target, _f.value.data)}) for operation, index, config in cls.write_indexes( (origin, [], property_map), None, False): if operation == 'ZADD': _flag, _index_key, value = 'Z', index[1], index[2] else: _flag, _index_key, value = 'S', index[1], index[2] if (_flag, _index_key) not in _filter_i_lookup: _filters[(_flag, _index_key)] = [] _filter_i_lookup.add((_flag, _index_key)) _filters[(_flag, _index_key)].append( (_f.operator, value, _f.chain)) # process sorted sets first: leads to lower cardinality sorted_indexes = dict([ (index, _filters[index]) for index in filter(lambda x: x[0] == 'Z', _filters.iterkeys()) ]) unsorted_indexes = dict([ (index, _filters[index]) for index in filter(lambda x: x[0] == 'S', _filters.iterkeys()) ]) _and_filters, _or_filters = [], [] if sorted_indexes: for prop, _directives in sorted_indexes.iteritems(): _operator, _value, chain = _directives[0] if chain: for subquery in chain: if subquery.sub_operator is query.AND: _and_filters.append(subquery) if subquery.sub_operator is query.OR: _or_filters.append(subquery) # double-filters if len(_filters[prop]) == 2: # extract info _operators = [ operator for operator, value in _directives ] _values = [value for operator, value in _directives] _, prop = prop # special case: maybe we can do a sorted range request if (query.GREATER_THAN in _operators) or (query.GREATER_THAN_EQUAL_TO in _operators): if (query.LESS_THAN in _operators) or (query.LESS_THAN_EQUAL_TO in _operators): # range value query over sorted index greater, lesser = max(_values), min(_values) _data_frame.append( cls.execute( *(cls.Operations.SORTED_RANGE_BY_SCORE, None, prop, min(_values), max(_values), options.offset, options.limit))) continue # single-filters if len(_filters[prop]) == 1: # extract info _operator = [ operator for operator, value in _directives ][0] _value = float( [value for operator, value in _directives][0]) _, prop = prop if _operator is query.EQUALS: # static value query over sorted index _data_frame.append( cls.execute( *(cls.Operations.SORTED_RANGE_BY_SCORE, None, prop, _value, _value, options.offset, options.limit))) continue ## @TODO(sgammon): build this query branch raise RuntimeError("Specified query is not yet supported.") if unsorted_indexes: _intersections = set() for prop, _directives in unsorted_indexes.iteritems(): _operator, _value, chain = _directives[0] if chain: for subquery in chain: if subquery.sub_operator is query.AND: _and_filters.append(subquery) if subquery.sub_operator is query.OR: _or_filters.append(subquery) _flag, index = prop _intersections.add(index) # special case: only one unsorted set - pull content instead of a intersection merge if _intersections and len(_intersections) == 1: _data_frame.append( cls.execute(*(cls.Operations.SET_MEMBERS, None, _intersections.pop()))) # more than one intersection: do an `SINTER` call instead of `SMEMBERS` if _intersections and len(_intersections) > 1: _data_frame.append( cls.execute(*(cls.Operations.SET_INTERSECT, None, _intersections))) if _data_frame: # there were results, start merging _result_window = set() for frame in _data_frame: if not len(_result_window): _result_window = set(frame) continue # initial frame: fill background _result_window = (_result_window & set(frame)) matching_keys = [k for k in _result_window] else: matching_keys = [] else: if not sorts: # grab all entities of this kind prefix = model.Key(kind, 0, parent=ancestry_parent).urlsafe().replace( '=', '')[0:-3] matching_keys = cls.execute(cls.Operations.KEYS, kind.kind(), prefix + '*') # regex search it. why not # if we're doing keys only, we're done if options.keys_only and not (_and_filters or _or_filters): return [ model.Key.from_urlsafe(k, _persisted=True) for k in matching_keys ] # otherwise, build entities and return result_entities = [] # fetch keys with cls.channel(kind.kind()).pipeline() as pipeline: # fill pipeline _seen_results = 0 for key in matching_keys: if (not (_and_filters or _or_filters)) and ( options.limit > 0 and _seen_results >= options.limit): break _seen_results += 1 cls.execute(cls.Operations.GET, kind.kind(), key, target=pipeline) _blob_results = pipeline.execute() # execute pipeline, zip keys and build results for key, entity in zip([ model.Key.from_urlsafe(k, _persisted=True) for k in matching_keys ], _blob_results): _seen_results = 0 if not entity: continue if (options.limit > 0) and _seen_results >= options.limit: break # decode raw entity decoded = cls.get(None, None, entity) # attach key, decode entity and construct decoded['key'] = key if _and_filters or _or_filters: if _and_filters and not all( (filter.match(decoded) for filter in _and_filters)): continue # doesn't match one of the filters if _or_filters and not any( (filter.match(decoded) for filter in _or_filters)): continue # doesn't match any of the `or` filters # matches, by the grace of god _seen_results += 1 result_entities.append(kind(_persisted=True, **decoded)) return result_entities