-
Notifications
You must be signed in to change notification settings - Fork 0
/
ladder.py
223 lines (179 loc) · 6.65 KB
/
ladder.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
"""Easy URL generation via object notation.
"""
from functools import partial
from ._compat import (
text_type,
iteritems,
urlencode,
urlsplit,
urlunsplit,
parse_qsl
)
class URLSplitParts(object):
"""Convert return from urlsplit into an updatable named attribute object.
"""
def __init__(self, scheme, netloc, path, query, fragment):
self.scheme = scheme
self.netloc = netloc
self.path = path
self.query = query
self.fragment = fragment
def __iter__(self):
return iter([
self.scheme,
self.netloc,
self.path,
self.query,
self.fragment
])
class URL(object):
"""Generate URLs using object notation."""
__attrs__ = ['__url__', '__params__', '__append_slash__']
def __init__(self, url=None, params=None, append_slash=False):
self.__url__ = urlpathjoin(url)
self.__append_slash__ = append_slash
self.__params__ = []
self.__setparams__(params)
def __str__(self):
return self.__geturl__()
def __repr__(self): # pragma: no cover
return '<{0} url={1}>'.format(
self.__class__.__name__, self.__geturl__())
def __add__(self, other):
return self(other)
__div__ = __add__
__truediv__ = __add__
def __radd__(self, other):
return self.__class__(other)(self)
__rdiv__ = __radd__
__rtruediv__ = __radd__
def __getstate__(self):
"""Return self.__attrs__ resolved onto self with leading/trailing `__`
removed. This is used to propagate init args to next URL generation.
"""
state = dict((attr.replace('__', ''), getattr(self, attr, None))
for attr in self.__attrs__)
return state
def __geturl__(self):
"""Return current URL as string. Combines query string parameters found
in string URL with any named parameters created during `__call__`."""
urlparts = self.__urlparts__
if self.__append_slash__ and not urlparts.path.endswith('/'):
urlparts.path = urlparts.path + '/'
urlparts.query = urlencode(self.__params__)
return urlunsplit(urlparts)
def __setparams__(self, params):
"""Extract any query string parameters from URL and merge with
`params`.
"""
urlparts = self.__urlparts__
if urlparts.query:
# move url query to params and remove it from url string
self.__params__ += parse_qsl(urlparts.query)
urlparts.query = None
self.__url__ = urlunsplit(urlparts)
if params:
if isinstance(params, dict):
params = list(iteritems(params))
self.__params__ += params
@property
def __urlsplit__(self):
"""Return urlsplit() of current URL."""
return urlsplit(self.__url__)
@property
def __urlparts__(self):
"""Return urlsplit as URLSplitParts object."""
return URLSplitParts(*self.__urlsplit__)
def __getattr__(self, path):
"""Treat attribute access as path concatenation."""
return self(path)
def __call__(self, *paths, **params):
"""Generate a new URL while extending the `url` with `path` and query
`params`.
"""
state = self.__getstate__()
state['url'] = urlpathjoin(state['url'], *paths)
state['params'] = state['params'] + list(iteritems(params))
# Use `__class__` to spawn new generation in case we are a subclass.
return self.__class__(**state)
class API(URL):
"""Add URL generation to an HTTP request client. Requires that the `client`
support HTTP verbs as lowercase methods. An example client would be the one
from Requests package.
"""
__attrs__ = [
'__client__',
'__url__',
'__params__',
'__append_slash__',
'__upper_methods__'
]
__http_methods__ = [
'head',
'options',
'get',
'post',
'put',
'patch',
'delete'
]
def __init__(self, client, url='', params=None,
append_slash=False, upper_methods=True):
super(API, self).__init__(url, params, append_slash)
self.__client__ = client
self.__upper_methods__ = upper_methods
# Dynamically set client proxy methods accessed during the getattr
# call.
self.__methods__ = [
method.upper() if self.__upper_methods__ else method
for method in self.__http_methods__
]
def __getattr__(self, attr):
if attr in self.__methods__:
# Proxy call to client method with url bound to first argument.
return partial(getattr(self.__client__, attr.lower()),
self.__geturl__())
else:
return super(API, self).__getattr__(attr)
def urlpathjoin(*paths):
"""Join URL paths into single URL while maintaining leading and trailing
slashes if present on first and last elements respectively.
>>> assert urlpathjoin('') == ''
>>> assert urlpathjoin(['', '/a']) == '/a'
>>> assert urlpathjoin(['a', '/']) == 'a/'
>>> assert urlpathjoin(['', '/a', '', '', 'b']) == '/a/b'
>>> assert urlpathjoin(['/a/', 'b/', '/c', 'd', 'e/']) == '/a/b/c/d/e/'
>>> assert urlpathjoin(['a', 'b', 'c']) == 'a/b/c'
>>> assert urlpathjoin(['a/b', '/c/d/', '/e/f']) == 'a/b/c/d/e/f'
>>> assert urlpathjoin('/', 'a', 'b', 'c', 1, '/') == '/a/b/c/1/'
>>> assert urlpathjoin([]) == ''
"""
paths = [text_type(path) for path in flatten(paths) if path]
if len(paths) == 1:
# Special case where there's no need to join anything.
# Doing this because if path==['/'], then an extra slash would be added
# if the else clause ran instead.
url = paths[0]
else:
leading = '/' if paths and paths[0].startswith('/') else ''
trailing = '/' if paths and paths[-1].endswith('/') else ''
url = (leading +
'/'.join([p.strip('/') for p in paths if p.strip('/')]) +
trailing)
return url
def iterflatten(items):
"""Return iterator which flattens list/tuple of lists/tuples
>>> to_flatten = [1, [2,3], [4, [5, [6]], 7], 8]
>>> assert list(iterflatten(to_flatten)) == [1,2,3,4,5,6,7,8]
"""
for item in items:
if isinstance(item, (list, tuple)):
for itm in flatten(item):
yield itm
else:
yield item
def flatten(items):
"""Return flattened list of a list/tuple of lists/tuples
>>> assert flatten([1, [2,3], [4, [5, [6]], 7], 8]) == [1,2,3,4,5,6,7,8]
"""
return list(iterflatten(items))