# Copyright (c) 2017 Ian Stapleton Cordasco # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """Module containing the logic for the URIBuilder object.""" from . import compat from . import normalizers from . import uri from . import uri_reference class URIBuilder: """Object to aid in building up a URI Reference from parts. .. note:: This object should be instantiated by the user, but it's recommended that it is not provided with arguments. Instead, use the available method to populate the fields. """ def __init__( self, scheme=None, userinfo=None, host=None, port=None, path=None, query=None, fragment=None, ): """Initialize our URI builder. :param str scheme: (optional) :param str userinfo: (optional) :param str host: (optional) :param int port: (optional) :param str path: (optional) :param str query: (optional) :param str fragment: (optional) """ self.scheme = scheme self.userinfo = userinfo self.host = host self.port = port self.path = path self.query = query self.fragment = fragment def __repr__(self): """Provide a convenient view of our builder object.""" formatstr = ( "URIBuilder(scheme={b.scheme}, userinfo={b.userinfo}, " "host={b.host}, port={b.port}, path={b.path}, " "query={b.query}, fragment={b.fragment})" ) return formatstr.format(b=self) @classmethod def from_uri(cls, reference): """Initialize the URI builder from another URI. Takes the given URI reference and creates a new URI builder instance populated with the values from the reference. If given a string it will try to convert it to a reference before constructing the builder. """ if not isinstance(reference, uri.URIReference): reference = uri_reference(reference) return cls( scheme=reference.scheme, userinfo=reference.userinfo, host=reference.host, port=reference.port, path=reference.path, query=reference.query, fragment=reference.fragment, ) def add_scheme(self, scheme): """Add a scheme to our builder object. After normalizing, this will generate a new URIBuilder instance with the specified scheme and all other attributes the same. .. code-block:: python >>> URIBuilder().add_scheme('HTTPS') URIBuilder(scheme='https', userinfo=None, host=None, port=None, path=None, query=None, fragment=None) """ scheme = normalizers.normalize_scheme(scheme) return URIBuilder( scheme=scheme, userinfo=self.userinfo, host=self.host, port=self.port, path=self.path, query=self.query, fragment=self.fragment, ) def add_credentials(self, username, password): """Add credentials as the userinfo portion of the URI. .. code-block:: python >>> URIBuilder().add_credentials('root', 's3crete') URIBuilder(scheme=None, userinfo='root:s3crete', host=None, port=None, path=None, query=None, fragment=None) >>> URIBuilder().add_credentials('root', None) URIBuilder(scheme=None, userinfo='root', host=None, port=None, path=None, query=None, fragment=None) """ if username is None: raise ValueError("Username cannot be None") userinfo = normalizers.normalize_username(username) if password is not None: userinfo = "{}:{}".format( userinfo, normalizers.normalize_password(password), ) return URIBuilder( scheme=self.scheme, userinfo=userinfo, host=self.host, port=self.port, path=self.path, query=self.query, fragment=self.fragment, ) def add_host(self, host): """Add hostname to the URI. .. code-block:: python >>> URIBuilder().add_host('google.com') URIBuilder(scheme=None, userinfo=None, host='google.com', port=None, path=None, query=None, fragment=None) """ return URIBuilder( scheme=self.scheme, userinfo=self.userinfo, host=normalizers.normalize_host(host), port=self.port, path=self.path, query=self.query, fragment=self.fragment, ) def add_port(self, port): """Add port to the URI. .. code-block:: python >>> URIBuilder().add_port(80) URIBuilder(scheme=None, userinfo=None, host=None, port='80', path=None, query=None, fragment=None) >>> URIBuilder().add_port(443) URIBuilder(scheme=None, userinfo=None, host=None, port='443', path=None, query=None, fragment=None) """ port_int = int(port) if port_int < 0: raise ValueError( "ports are not allowed to be negative. You provided {}".format( port_int, ) ) if port_int > 65535: raise ValueError( "ports are not allowed to be larger than 65535. " "You provided {}".format( port_int, ) ) return URIBuilder( scheme=self.scheme, userinfo=self.userinfo, host=self.host, port=f"{port_int}", path=self.path, query=self.query, fragment=self.fragment, ) def add_path(self, path): """Add a path to the URI. .. code-block:: python >>> URIBuilder().add_path('sigmavirus24/rfc3985') URIBuilder(scheme=None, userinfo=None, host=None, port=None, path='/sigmavirus24/rfc3986', query=None, fragment=None) >>> URIBuilder().add_path('/checkout.php') URIBuilder(scheme=None, userinfo=None, host=None, port=None, path='/checkout.php', query=None, fragment=None) """ if not path.startswith("/"): path = f"/{path}" return URIBuilder( scheme=self.scheme, userinfo=self.userinfo, host=self.host, port=self.port, path=normalizers.normalize_path(path), query=self.query, fragment=self.fragment, ) def extend_path(self, path): """Extend the existing path value with the provided value. .. versionadded:: 1.5.0 .. code-block:: python >>> URIBuilder(path="/users").extend_path("/sigmavirus24") URIBuilder(scheme=None, userinfo=None, host=None, port=None, path='/users/sigmavirus24', query=None, fragment=None) >>> URIBuilder(path="/users/").extend_path("/sigmavirus24") URIBuilder(scheme=None, userinfo=None, host=None, port=None, path='/users/sigmavirus24', query=None, fragment=None) >>> URIBuilder(path="/users/").extend_path("sigmavirus24") URIBuilder(scheme=None, userinfo=None, host=None, port=None, path='/users/sigmavirus24', query=None, fragment=None) >>> URIBuilder(path="/users").extend_path("sigmavirus24") URIBuilder(scheme=None, userinfo=None, host=None, port=None, path='/users/sigmavirus24', query=None, fragment=None) """ existing_path = self.path or "" path = "{}/{}".format(existing_path.rstrip("/"), path.lstrip("/")) return self.add_path(path) def add_query_from(self, query_items): """Generate and add a query a dictionary or list of tuples. .. code-block:: python >>> URIBuilder().add_query_from({'a': 'b c'}) URIBuilder(scheme=None, userinfo=None, host=None, port=None, path=None, query='a=b+c', fragment=None) >>> URIBuilder().add_query_from([('a', 'b c')]) URIBuilder(scheme=None, userinfo=None, host=None, port=None, path=None, query='a=b+c', fragment=None) """ query = normalizers.normalize_query(compat.urlencode(query_items)) return URIBuilder( scheme=self.scheme, userinfo=self.userinfo, host=self.host, port=self.port, path=self.path, query=query, fragment=self.fragment, ) def extend_query_with(self, query_items): """Extend the existing query string with the new query items. .. versionadded:: 1.5.0 .. code-block:: python >>> URIBuilder(query='a=b+c').extend_query_with({'a': 'b c'}) URIBuilder(scheme=None, userinfo=None, host=None, port=None, path=None, query='a=b+c&a=b+c', fragment=None) >>> URIBuilder(query='a=b+c').extend_query_with([('a', 'b c')]) URIBuilder(scheme=None, userinfo=None, host=None, port=None, path=None, query='a=b+c&a=b+c', fragment=None) """ original_query_items = compat.parse_qsl(self.query or "") if not isinstance(query_items, list): query_items = list(query_items.items()) return self.add_query_from(original_query_items + query_items) def add_query(self, query): """Add a pre-formated query string to the URI. .. code-block:: python >>> URIBuilder().add_query('a=b&c=d') URIBuilder(scheme=None, userinfo=None, host=None, port=None, path=None, query='a=b&c=d', fragment=None) """ return URIBuilder( scheme=self.scheme, userinfo=self.userinfo, host=self.host, port=self.port, path=self.path, query=normalizers.normalize_query(query), fragment=self.fragment, ) def add_fragment(self, fragment): """Add a fragment to the URI. .. code-block:: python >>> URIBuilder().add_fragment('section-2.6.1') URIBuilder(scheme=None, userinfo=None, host=None, port=None, path=None, query=None, fragment='section-2.6.1') """ return URIBuilder( scheme=self.scheme, userinfo=self.userinfo, host=self.host, port=self.port, path=self.path, query=self.query, fragment=normalizers.normalize_fragment(fragment), ) def finalize(self): """Create a URIReference from our builder. .. code-block:: python >>> URIBuilder().add_scheme('https').add_host('github.com' ... ).add_path('sigmavirus24/rfc3986').finalize().unsplit() 'https://github.com/sigmavirus24/rfc3986' >>> URIBuilder().add_scheme('https').add_host('github.com' ... ).add_path('sigmavirus24/rfc3986').add_credentials( ... 'sigmavirus24', 'not-re@l').finalize().unsplit() 'https://sigmavirus24:not-re%40l@github.com/sigmavirus24/rfc3986' """ return uri.URIReference( self.scheme, normalizers.normalize_authority( (self.userinfo, self.host, self.port) ), self.path, self.query, self.fragment, ) def geturl(self): """Generate the URL from this builder. .. versionadded:: 1.5.0 This is an alternative to calling :meth:`finalize` and keeping the :class:`rfc3986.uri.URIReference` around. """ return self.finalize().unsplit()