e996204a07afe31e7c7972e8794c90c13d295248
[sdc/sdc-distribution-client.git] /
1 from __future__ import absolute_import
2 from collections import namedtuple
3
4 from ..exceptions import LocationParseError
5
6
7 url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment']
8
9
10 class Url(namedtuple('Url', url_attrs)):
11     """
12     Datastructure for representing an HTTP URL. Used as a return value for
13     :func:`parse_url`.
14     """
15     slots = ()
16
17     def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None,
18                 query=None, fragment=None):
19         if path and not path.startswith('/'):
20             path = '/' + path
21         return super(Url, cls).__new__(cls, scheme, auth, host, port, path,
22                                        query, fragment)
23
24     @property
25     def hostname(self):
26         """For backwards-compatibility with urlparse. We're nice like that."""
27         return self.host
28
29     @property
30     def request_uri(self):
31         """Absolute path including the query string."""
32         uri = self.path or '/'
33
34         if self.query is not None:
35             uri += '?' + self.query
36
37         return uri
38
39     @property
40     def netloc(self):
41         """Network location including host and port"""
42         if self.port:
43             return '%s:%d' % (self.host, self.port)
44         return self.host
45
46     @property
47     def url(self):
48         """
49         Convert self into a url
50
51         This function should more or less round-trip with :func:`.parse_url`. The
52         returned url may not be exactly the same as the url inputted to
53         :func:`.parse_url`, but it should be equivalent by the RFC (e.g., urls
54         with a blank port will have : removed).
55
56         Example: ::
57
58             >>> U = parse_url('http://google.com/mail/')
59             >>> U.url
60             'http://google.com/mail/'
61             >>> Url('http', 'username:password', 'host.com', 80,
62             ... '/path', 'query', 'fragment').url
63             'http://username:password@host.com:80/path?query#fragment'
64         """
65         scheme, auth, host, port, path, query, fragment = self
66         url = ''
67
68         # We use "is not None" we want things to happen with empty strings (or 0 port)
69         if scheme is not None:
70             url += scheme + '://'
71         if auth is not None:
72             url += auth + '@'
73         if host is not None:
74             url += host
75         if port is not None:
76             url += ':' + str(port)
77         if path is not None:
78             url += path
79         if query is not None:
80             url += '?' + query
81         if fragment is not None:
82             url += '#' + fragment
83
84         return url
85
86     def __str__(self):
87         return self.url
88
89
90 def split_first(s, delims):
91     """
92     Given a string and an iterable of delimiters, split on the first found
93     delimiter. Return two split parts and the matched delimiter.
94
95     If not found, then the first part is the full input string.
96
97     Example::
98
99         >>> split_first('foo/bar?baz', '?/=')
100         ('foo', 'bar?baz', '/')
101         >>> split_first('foo/bar?baz', '123')
102         ('foo/bar?baz', '', None)
103
104     Scales linearly with number of delims. Not ideal for large number of delims.
105     """
106     min_idx = None
107     min_delim = None
108     for d in delims:
109         idx = s.find(d)
110         if idx < 0:
111             continue
112
113         if min_idx is None or idx < min_idx:
114             min_idx = idx
115             min_delim = d
116
117     if min_idx is None or min_idx < 0:
118         return s, '', None
119
120     return s[:min_idx], s[min_idx + 1:], min_delim
121
122
123 def parse_url(url):
124     """
125     Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is
126     performed to parse incomplete urls. Fields not provided will be None.
127
128     Partly backwards-compatible with :mod:`urlparse`.
129
130     Example::
131
132         >>> parse_url('http://google.com/mail/')
133         Url(scheme='http', host='google.com', port=None, path='/mail/', ...)
134         >>> parse_url('google.com:80')
135         Url(scheme=None, host='google.com', port=80, path=None, ...)
136         >>> parse_url('/foo?bar')
137         Url(scheme=None, host=None, port=None, path='/foo', query='bar', ...)
138     """
139
140     # While this code has overlap with stdlib's urlparse, it is much
141     # simplified for our needs and less annoying.
142     # Additionally, this implementations does silly things to be optimal
143     # on CPython.
144
145     if not url:
146         # Empty
147         return Url()
148
149     scheme = None
150     auth = None
151     host = None
152     port = None
153     path = None
154     fragment = None
155     query = None
156
157     # Scheme
158     if '://' in url:
159         scheme, url = url.split('://', 1)
160
161     # Find the earliest Authority Terminator
162     # (http://tools.ietf.org/html/rfc3986#section-3.2)
163     url, path_, delim = split_first(url, ['/', '?', '#'])
164
165     if delim:
166         # Reassemble the path
167         path = delim + path_
168
169     # Auth
170     if '@' in url:
171         # Last '@' denotes end of auth part
172         auth, url = url.rsplit('@', 1)
173
174     # IPv6
175     if url and url[0] == '[':
176         host, url = url.split(']', 1)
177         host += ']'
178
179     # Port
180     if ':' in url:
181         _host, port = url.split(':', 1)
182
183         if not host:
184             host = _host
185
186         if port:
187             # If given, ports must be integers.
188             if not port.isdigit():
189                 raise LocationParseError(url)
190             port = int(port)
191         else:
192             # Blank ports are cool, too. (rfc3986#section-3.2.3)
193             port = None
194
195     elif not host and url:
196         host = url
197
198     if not path:
199         return Url(scheme, auth, host, port, path, query, fragment)
200
201     # Fragment
202     if '#' in path:
203         path, fragment = path.split('#', 1)
204
205     # Query
206     if '?' in path:
207         path, query = path.split('?', 1)
208
209     return Url(scheme, auth, host, port, path, query, fragment)
210
211
212 def get_host(url):
213     """
214     Deprecated. Use :func:`.parse_url` instead.
215     """
216     p = parse_url(url)
217     return p.scheme or 'http', p.hostname, p.port