Skip to content
This repository
  • 2 commits
  • 3 files changed
  • 0 comments
  • 2 contributors
Aug 10, 2012
Burak Yiğit Kaya Implement multiple pk support
Not all entities consist of a single unique primary key but sometimes a combination of keys like a repository
or branch in a repository on GitHub. This patch adds native multiple pk support so that you can do

    github.Branch.get('BYK', 'pyresto', 'multiple-pk')
961f09f
Berker Peksag berkerpeksag Merge pull request #28 from BYK/multiple-pk
Implement multiple pk support.
b3b2229
16 docs/tutorial.rst
Source Rendered
@@ -17,7 +17,7 @@ will hold the common values such as the API host, the common model
17 17 representation using ``__repr__`` etc:
18 18
19 19 .. literalinclude:: ../pyresto/apis/github/__init__.py
20   - :lines: 7-14
  20 + :lines: 7-18
21 21
22 22
23 23 Simple Models
@@ -27,7 +27,7 @@ Then continue with implementing simple models which does not refer to any other
27 27 model, such as the ``Comment`` model for GitHub:
28 28
29 29 .. literalinclude:: ../pyresto/apis/github/__init__.py
30   - :lines: 17-19
  30 + :lines: 21-23
31 31
32 32
33 33 Note that we didn't define *any* attributes except for the mandatory ``_path``
@@ -47,7 +47,7 @@ After defining some "simple" models, you can start implementing models having
47 47 relations with each other:
48 48
49 49 .. literalinclude:: ../pyresto/apis/github/__init__.py
50   - :lines: 22-25
  50 + :lines: 26-29
51 51
52 52 Note that we used the attribute name ``comments`` which will "shadow" any
53 53 attribute named "comments" sent by the server as documented in
@@ -74,7 +74,7 @@ If we were expecting lots of items to be in the collection, or an unknown
74 74 number of items in the collection, we could have used ``lazy=True`` like this:
75 75
76 76 .. literalinclude:: ../pyresto/apis/github/__init__.py
77   - :lines: 47-54
  77 + :lines: 51-59
78 78
79 79 Using ``lazy=True`` will result in a :class:`LazyList<.core.LazyList>` type of
80 80 field on the model when accessed, which is basically a generator. So you can
@@ -85,7 +85,7 @@ You can also use the :class:`Foreign<.core.Foreign>` relation to refer to
85 85 other models:
86 86
87 87 .. literalinclude:: ../pyresto/apis/github/__init__.py
88   - :lines: 36-39
  88 + :lines: 40-43
89 89
90 90 When used in its simplest form, just like in the code above, this relation
91 91 expects the primary key value for the model it is referencing, ``Commit`` here,
@@ -105,7 +105,7 @@ not always possible to put all relation definitons inside the class definition.
105 105 For those cases, you can simply late bind the relations as follows:
106 106
107 107 .. literalinclude:: ../pyresto/apis/github/__init__.py
108   - :lines: 74-79
  108 + :lines: 79-87
109 109
110 110
111 111 Authentication
@@ -116,7 +116,7 @@ means of authentication is essential. Define the possible authentication
116 116 mechanisms for the service:
117 117
118 118 .. literalinclude:: ../pyresto/apis/github/__init__.py
119   - :lines: 4,81-82
  119 + :lines: 4,89-81
120 120
121 121 Make sure you use the provided authentication classes by :mod:`requests.auth`
122 122 if they suit your needs. If you still need a custom authentication class, make
@@ -127,7 +127,7 @@ will set the default authentication method and credentials for all requests for
127 127 convenience:
128 128
129 129 .. literalinclude:: ../pyresto/apis/github/__init__.py
130   - :lines: 84-85
  130 + :lines: 92-93
131 131
132 132 Above, we provide the list of methods/classes we have previously defined, the
133 133 base class for our service since all other models inherit from that and will
60 pyresto/apis/github/__init__.py
@@ -8,35 +8,39 @@ class GitHubModel(Model):
8 8 _url_base = 'https://api.github.com'
9 9
10 10 def __repr__(self):
11   - descriptor = getattr(self, "url",
12   - '`{0}`: {1}'.format(self._pk, self._id))
  11 + if hasattr(self, '_links'):
  12 + desc = self._links['self']
  13 + elif hasattr(self, 'url'):
  14 + desc = self.url
  15 + else:
  16 + desc = self._current_path
13 17
14   - return '<GitHub.{0} [{1}]>'.format(self.__class__.__name__, descriptor)
  18 + return '<GitHub.{0} [{1}]>'.format(self.__class__.__name__, desc)
15 19
16 20
17 21 class Comment(GitHubModel):
18 22 _path = '/repos/{user}/{repo}/comments/{id}'
19   - _pk = 'id'
  23 + _pk = ('user', 'repo', 'id')
20 24
21 25
22 26 class Commit(GitHubModel):
23 27 _path = '/repos/{user}/{repo}/commits/{sha}'
24   - _pk = 'sha'
25   - comments = Many(Comment, '{commit.url}/comments?per_page=100')
  28 + _pk = ('user', 'repo', 'sha')
  29 + comments = Many(Comment, '{self._current_path}/comments?per_page=100')
26 30
27 31
28 32 class Branch(GitHubModel):
29 33 _path = '/repos/{user}/{repo}/branches/{name}'
30   - _pk = 'name'
31   - commit = Foreign(Commit)
32   - commits = Many(Commit, '{repo.url}/commits?per_page=100&sha={branch._id}',
33   - lazy=True)
  34 + _pk = ('user', 'repo', 'name')
  35 + commit = Foreign(Commit, embedded=True)
  36 + commits = Many(Commit, '/repos/{user}/{repo}/commits'
  37 + '?per_page=100&sha={self._id}', lazy=True)
34 38
35 39
36 40 class Tag(GitHubModel):
37   - _path = '/repos/{user}/{repo}/tags/{id}'
38   - _pk = 'name'
39   - commit = Foreign(Commit)
  41 + _path = '/repos/{user}/{repo}/tags/{name}'
  42 + _pk = ('user', 'repo', 'name')
  43 + commit = Foreign(Commit, embedded=True)
40 44
41 45
42 46 class Key(GitHubModel):
@@ -46,25 +50,26 @@ class Key(GitHubModel):
46 50
47 51 class Repo(GitHubModel):
48 52 _path = '/repos/{user}/{name}'
49   - _pk = 'name'
50   - commits = Many(Commit, '{repo.url}/commits?per_page=100', lazy=True)
51   - comments = Many(Comment, '{repo.url}/comments?per_page=100')
52   - tags = Many(Tag, '{repo.url}/tags?per_page=100')
53   - branches = Many(Branch, '{repo.url}/branches?per_page=100')
54   - keys = Many(Key, '{repo.url}/keys?per_page=100')
  53 + _pk = ('user', 'name')
  54 + commits = Many(Commit, '{self._current_path}/commits?per_page=100',
  55 + lazy=True)
  56 + comments = Many(Comment, '{self._current_path}/comments?per_page=100')
  57 + tags = Many(Tag, '{self._current_path}/tags?per_page=100')
  58 + branches = Many(Branch, '{self._current_path}/branches?per_page=100')
  59 + keys = Many(Key, '{self._current_path}/keys?per_page=100')
55 60
56 61
57 62 class User(GitHubModel):
58 63 _path = '/users/{login}'
59 64 _pk = 'login'
60 65
61   - repos = Many(Repo, '{user.url}/repos?type=all&per_page=100')
62   - keys = Many(Key, '/user/keys?per_page=100')
  66 + repos = Many(Repo, '{self._current_path}/repos?type=all&per_page=100')
63 67
64 68
65 69 class Me(User):
66 70 _path = '/user'
67 71 repos = Many(Repo, '/user/repos?type=all&per_page=100')
  72 + keys = Many(Key, '/user/keys?per_page=100')
68 73
69 74 @classmethod
70 75 def get(cls, **kwargs):
@@ -72,11 +77,14 @@ def get(cls, **kwargs):
72 77
73 78
74 79 # Late bindings due to circular references
75   -Repo.contributors = Many(User, '{repo.url}/contributors?per_page=100')
76   -Repo.owner = Foreign(User, 'owner')
77   -Repo.watcher_list = Many(User, '{repo.url}/watchers?per_page=100')
78   -User.follower_list = Many(User, '{user.url}/followers?per_page=100')
79   -User.watched = Many(Repo, '{user.url}/watched?per_page=100')
  80 +Commit.committer = Foreign(User, '__committer', embedded=True)
  81 +Commit.author = Foreign(User, '__author', embedded=True)
  82 +Repo.contributors = Many(User,
  83 + '{self._current_path}/contributors?per_page=100')
  84 +Repo.owner = Foreign(User, '__owner', embedded=True)
  85 +Repo.watcher_list = Many(User, '{self._current_path}/watchers?per_page=100')
  86 +User.follower_list = Many(User, '{self._current_path}/followers?per_page=100')
  87 +User.watched = Many(Repo, '{self._current_path}/watched?per_page=100')
80 88
81 89 # Define authentication methods
82 90 auths = AuthList(basic=HTTPBasicAuth)
151 pyresto/core.py
@@ -67,6 +67,9 @@ def __new__(mcs, name, bases, attrs):
67 67 if not hasattr(new_class, '_path'): # don't override if defined
68 68 new_class._path = u'/{0}/{{1:id}}'.format(quote(name.lower()))
69 69
  70 + if not isinstance(new_class._pk, tuple): # make sure it is a tuple
  71 + new_class._pk = (new_class._pk,)
  72 +
70 73 return new_class
71 74
72 75
@@ -164,7 +167,7 @@ class AuthList(dict):
164 167 :data:`apis.github.auths` for example usage.
165 168
166 169 .. literalinclude:: ../pyresto/apis/github/__init__.py
167   - :lines: 81-82
  170 + :lines: 89-90
168 171
169 172 """
170 173 def __getattr__(self, attr):
@@ -180,7 +183,7 @@ def enable_auth(supported_types, base_model, default_type):
180 183 :func:`apis.github.auth` for example usage.
181 184
182 185 .. literalinclude:: ../pyresto/apis/github/__init__.py
183   - :lines: 84-85
  186 + :lines: 92-93
184 187
185 188 :param supported_types: A dict of supported types as ``"name": AuthClass``
186 189 pairs
@@ -247,7 +250,7 @@ def __init__(self, model, path=None, lazy=False):
247 250 """
248 251
249 252 self.__model = model
250   - self.__path = unicode(path) or model._path # ensure unicode
  253 + self.__path = path or model._path # ensure unicode
251 254 self.__lazy = lazy
252 255 self.__cache = dict()
253 256
@@ -313,12 +316,7 @@ def __get__(self, instance, owner):
313 316 if instance not in self.__cache:
314 317 model = self.__model
315 318
316   - # Get the necessary dict object collected from the chain of Models
317   - # to properly populate the collection path
318   - path_params = instance._parents
319   - if hasattr(instance, '_get_params'):
320   - path_params.update(instance._get_params)
321   - path = self.__path.format(**path_params)
  319 + path = self.__path.format(**instance._footprint)
322 320
323 321 if self.__lazy:
324 322 self.__cache[instance] = LazyList(self._with_owner(instance),
@@ -340,7 +338,8 @@ class Foreign(Relation):
340 338
341 339 """
342 340
343   - def __init__(self, model, key_property=None, key_extractor=None):
  341 + def __init__(self, model, key_property=None, key_extractor=None,
  342 + embedded=False):
344 343 """
345 344 Constructor for the :class:`Foreign` relations.
346 345
@@ -362,13 +361,30 @@ def __init__(self, model, key_property=None, key_extractor=None):
362 361 """
363 362
364 363 self.__model = model
365   - if not key_property:
366   - key_property = model.__name__.lower()
367   - model_pk = model._pk
368   - self.__key_extractor = (key_extractor if key_extractor else
369   - lambda x: dict(model_pk=getattr(x, '__' + key_property)[model_pk]))
370   -
371 364 self.__cache = dict()
  365 + self.__embedded = embedded and not key_extractor
  366 +
  367 + self.__key_property = key_property or '__' + model.__name__.lower()
  368 +
  369 + if key_extractor:
  370 + self.__key_extractor = key_extractor
  371 + elif not embedded:
  372 + def extract(instance):
  373 + footprint = instance._footprint
  374 + ids = list()
  375 +
  376 + for k in self.__model._pk[:-1]:
  377 + ids.append(footprint[k] if k in footprint
  378 + else getattr(instance, k))
  379 +
  380 + item, key = re.match(r'(\w+)(?:\[(\w+)\])?',
  381 + key_property).groups()
  382 + item = getattr(instance, item)
  383 + ids.append(item[key] if key else item)
  384 +
  385 + return tuple(ids)
  386 +
  387 + self.__key_extractor = extract
372 388
373 389 def __get__(self, instance, owner):
374 390 # Please see Many.__get__ for more info on this method.
@@ -376,11 +392,15 @@ def __get__(self, instance, owner):
376 392 return self.__model
377 393
378 394 if instance not in self.__cache:
379   - keys = instance._parents
380   - keys.update(self.__key_extractor(instance))
381   - pk = keys.pop(self.__model._pk)
382   - self.__cache[instance] = self.__model.get(pk, auth=instance._auth,
383   - **keys)
  395 + if self.__embedded:
  396 + self.__cache[instance] = self.__model(
  397 + **getattr(instance, self.__key_property))
  398 + self.__cache[instance]._auth = instance._auth
  399 + else:
  400 + self.__cache[instance] = self.__model.get(
  401 + *self.__key_extractor(instance), auth=instance._auth)
  402 +
  403 + self.__cache[instance]._pyresto_owner = instance
384 404
385 405 return self.__cache[instance]
386 406
@@ -395,6 +415,10 @@ class Model(object):
395 415
396 416 __metaclass__ = ModelBase
397 417
  418 + __footprint = None
  419 +
  420 + __pk_vals = None
  421 +
398 422 #: The class variable that holds the bae uel for the API endpoint for the
399 423 #: :class:`Model`. This should be a "full" URL including the scheme, port
400 424 #: and the initial path if there is any.
@@ -475,8 +499,6 @@ def _pk(self):
475 499 #: current :class:`Model` instance while fetching its related resources.
476 500 _get_params = dict()
477 501
478   - __ids = None
479   -
480 502 def __init__(self, **kwargs):
481 503 """
482 504 Constructor for model instances. All named parameters passed to this
@@ -504,34 +526,43 @@ def __init__(self, **kwargs):
504 526 if issubclass(getattr(cls, item), Model):
505 527 self.__dict__['__' + item] = self.__dict__.pop(item)
506 528
507   - try:
508   - self._current_path = self._path and (
509   - self._path.format(**self.__dict__))
510   - except KeyError:
511   - self._current_path = None
512   -
513 529 @property
514 530 def _id(self):
515 531 """A property that returns the instance's primary key value."""
516   - return getattr(self, self._pk)
  532 + if self.__pk_vals:
  533 + return self.__pk_vals[-1]
  534 + else: # assuming last pk is defined on self!
  535 + return getattr(self, self._pk[-1])
517 536
518 537 @property
519   - def _parents(self):
520   - """
521   - A property that returns a look-up dictionary for all parents of the
522   - current instance. Uses lower-cased class names for keys and the
523   - instance references as their values.
  538 + def _pk_vals(self):
  539 + if not self.__pk_vals:
  540 + if hasattr(self, '_pyresto_owner'):
  541 + self.__pk_vals = self._pyresto_owner.\
  542 + _pk_vals[:len(self._pk) - 1] + (self._id,)
  543 + else:
  544 + self.__pk_vals = (None,) * (len(self._pk) - 1) + (self._id,)
524 545
525   - """
  546 + return self.__pk_vals
526 547
527   - if self.__ids is None:
528   - self.__ids = dict()
529   - owner = self
530   - while owner:
531   - self.__ids[owner.__class__.__name__.lower()] = owner
532   - owner = getattr(owner, '_pyresto_owner', None)
  548 + @_pk_vals.setter
  549 + def _pk_vals(self, value):
  550 + if len(value) == len(self._pk):
  551 + self.__pk_vals = tuple(value)
  552 + else:
  553 + raise ValueError
533 554
534   - return self.__ids
  555 + @property
  556 + def _footprint(self):
  557 + if not self.__footprint:
  558 + self.__footprint = dict(zip(self._pk, self._pk_vals))
  559 + self.__footprint['self'] = self
  560 +
  561 + return self.__footprint
  562 +
  563 + @property
  564 + def _current_path(self):
  565 + return self._path.format(**self._footprint)
535 566
536 567 @classmethod
537 568 def _get_sanitized_url(cls, url):
@@ -595,19 +626,19 @@ def _rest_call(cls, url, method='GET', fetch_all=True, **kwargs):
595 626 'Response code: {0:d}'.format(response.status_code))
596 627
597 628 def __fetch(self):
598   - # if we don't have a path then we cannot fetch anything since we don't
599   - # know the address of the resource.
600   - if not self._current_path:
601   - self._fetched = True
602   - return
603   -
604 629 data, next_url = self._rest_call(url=self._current_path,
605 630 auth=self._auth)
606   - if next_url:
607   - self._current_path = next_url
608 631
609 632 if data:
610 633 self.__dict__.update(data)
  634 +
  635 + cls = self.__class__
  636 + overlaps = set(cls.__dict__) & set(data)
  637 +
  638 + for item in overlaps:
  639 + if issubclass(getattr(cls, item), Model):
  640 + self.__dict__['__' + item] = self.__dict__.pop(item)
  641 +
611 642 self._fetched = True
612 643
613 644 def __getattr__(self, name):
@@ -620,17 +651,16 @@ def __eq__(self, other):
620 651 return isinstance(other, self.__class__) and self._id == other._id
621 652
622 653 def __repr__(self):
623   - if self._current_path:
  654 + if self._path:
624 655 descriptor = self._current_path
625 656 else:
626   - descriptor = ' - {0}: {1}'.format(self._pk, self._id)
  657 + descriptor = ' - {0}'.format(self._footprint)
627 658
628   - return '<Pyresto.Model.{0} [{1}{2}]>'.format(self.__class__.__name__,
629   - self._url_base,
630   - descriptor)
  659 + return '<Pyresto.Model.{0} [{1}]>'.format(self.__class__.__name__,
  660 + descriptor)
631 661
632 662 @classmethod
633   - def get(cls, pk, **kwargs):
  663 + def get(cls, *args, **kwargs):
634 664 """
635 665 The class method that fetches and instantiates the resource defined by
636 666 the provided pk value. Any other extra keyword arguments are used to
@@ -644,15 +674,16 @@ def get(cls, pk, **kwargs):
644 674 """
645 675
646 676 auth = kwargs.pop('auth', cls._auth)
647   - kwargs[cls._pk] = pk
648   - path = cls._path.format(**kwargs)
  677 +
  678 + ids = dict(zip(cls._pk, args))
  679 + path = cls._path.format(**ids)
649 680 data = cls._rest_call(url=path, auth=auth).data
650 681
651 682 if not data:
652 683 return
653 684
654 685 instance = cls(**data)
655   - instance._get_params = kwargs
  686 + instance._pk_vals = args
656 687 instance._fetched = True
657 688 if auth:
658 689 instance._auth = auth

No commit comments for this range

Something went wrong with that request. Please try again.