-
Notifications
You must be signed in to change notification settings - Fork 0
/
scopes.py
234 lines (175 loc) · 6.94 KB
/
scopes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import sys
from collections import namedtuple
from itertools import chain, repeat
if sys.version_info < (3, 0):
from collections import Set as AbstractSet
texttype = (unicode, str) # noqa
else:
from collections.abc import Set as AbstractSet
texttype = str
__all__ = ['Set', 'Item']
class Item(namedtuple("Item", "nodes permissions")):
@classmethod
def parse(cls, s, child_sep=u"/", permission_sep=u"+",
default_permissions=u"r"):
node_part, _, permission_part = s.partition(permission_sep)
if not permission_part:
permission_part = default_permissions
return cls(node_part.split(child_sep), set(permission_part))
def format(self, child_sep=u"/", permission_sep=u"+",
default_permissions=u"r"):
node_part = child_sep.join(self.nodes)
permissions_part = u"".join(self.permissions)
if permissions_part == default_permissions:
return node_part
else:
return permission_sep.join((node_part, permissions_part))
def parents(self, other):
"""
Return whether this Item is a parent of another Item
(ignores permissions)
"""
return other.nodes[:len(self.nodes)] == self.nodes
def rejects(self, other):
"""
Return which permissions of the other item this does not provide
"""
return other.permissions - self.permissions
def __contains__(self, other):
return self.parents(other) and not self.rejects(other)
class Set(AbstractSet):
"""Helper class for checking scope items.
scopes.Set implements the ``__contains__`` magic method, making it easy
to check if a particular scope and permission is expressed in a set of
scopes.
>>> from scopes import Set
>>> Set(['user/emails+r'])
Set(['user/emails'])
>>> 'user/emails' in Set(['user/emails'])
True
>>> ('foo/bar', 'foo/baz') <= Set('foo')
True
>>> ['foo/bar', 'foo/baz', 'extra'] <= Set(['foo', 'bar'])
False
>>> Set(['foo', 'bar']) >= ('foo/bar', 'foo/baz')
True
A Set in fact works almost like any set type.
>>> len(Set(['user/emails', 'user/repo']))
2
>>> list(Set(['user/emails+r', 'user/repo+aaaaa']).formatted())
['user/emails', 'user/repo+a']
They can be quickly parsed from strings too.
>>> Set("user/emails+r user/emails+n")
Set(['user/emails', 'user/emails+n'])
This method uses a single space as a separator.
Permissions
~~~~~~~~~~~
You can append letters to scope items to express certain permissions.
Any ascii letter that follows the permission separator (``+`` by
default) is interpreted as a permission. When checking for an item
in the scope list, both its value and permission must match at least
one item in the list.
>>> 'user/emails+a' in Set(['user/emails'])
False
>>> 'user/emails+a' in Set(['user/emails+a'])
True
Indicate multiple permissions in a scope list item by including more than
one letter after the ``+`` symbol. Duplicate permissions are ignored.
>>> 'user/repo+w' in Set(['user/repo+abcd', 'user/repo+rw'])
True
Permissions are totally arbitrary, except that ``+r`` is assumed by
default when no permissions are explicitly given.
>>> 'user/emails+r' in Set(['user/emails'])
True
You can change the default permissions to whatever you like.
>>> 'user/emails+n' in Set(['user/emails'], default_permissions='n')
True
>>> 'user/emails+q' in Set(['user/emails'], default_permissions='pq')
True
>>> 'user/emails+p' in Set(['user/emails'], default_permissions='pq')
True
The permissions separator is also configurable.
>>> 'user/emails|r' in Set(['user/emails'], permission_sep='|')
True
Parents
~~~~~~~
The ``/`` symbol is the default child separator. Parent scope items
automatically 'contain' child items in the scope list.
>>> 'user/emails+r' in Set(['user'])
True
>>> 'user/emails+w' in Set(['user'])
False
>>> 'user/emails+rw' in Set(['user+w', 'user/emails+r'])
True
The child separator can also be changed:
>>> 'user:emails+r' in Set(['user'], child_sep=':')
True
"""
def __init__(self, items, child_sep=u"/", permission_sep=u"+",
default_permissions=u"r"):
self.child_sep = child_sep
self.permission_sep = permission_sep
self.default_permissions = default_permissions
if isinstance(items, texttype):
items = items.split()
self.items = tuple(self._make_item(o) for o in items)
def __contains__(self, item):
# Coerce the argument to an Item instance
item = self._make_item(item)
# Find parent items, and for each one, mark the required permissions
# that it provides as met
parents = (o for o in self.items if o.parents(item))
for parent in parents:
unmet_permissions = parent.rejects(item)
# If all the permissions have been met, time to go
if not unmet_permissions:
return True
else:
# update item so that it only looks for those missing
# permissions; not ones which have already been met
item = Item(item.nodes, unmet_permissions)
else:
# The entire set of parents is traversed, but there are
# unmet permissions
return False
def __len__(self):
return len(self.items)
def __iter__(self):
return iter(self.items)
def __repr__(self):
return "Set({!r})".format(list(self.formatted()))
def __ge__(self, other):
if not isinstance(other, Set):
other = Set(other)
self = self._expanded_to_match(other)
return super(Set, self).__ge__(other)
def _expanded_to_match(self, other):
# In order to get around an optimisation by collections.abc.Set,
# must make sure the other is not larger that this, even if it
# means adding dummy items
o = self
differential = len(other) - len(o)
if differential > 0:
dummy = Item([], set())
o = Set(chain(o.items, repeat(dummy, differential)))
return o
def formatted(self):
for item in self.items:
yield item.format(
child_sep=self.child_sep,
permission_sep=self.permission_sep,
default_permissions=self.default_permissions
)
def _make_item(self, o):
# Convert an object to an item if it isn't already
if isinstance(o, Item):
return o
elif isinstance(o, texttype):
return Item.parse(
o,
child_sep=self.child_sep,
permission_sep=self.permission_sep,
default_permissions=self.default_permissions
)
else:
raise TypeError("object not supported scope item: {0}".format(o))