"""BuildURL's core."""
from copy import deepcopy
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
# Type aliases
PathList = List[str]
Path = Union[str, PathList]
QueryDict = Dict[str, Any]
Query = Union[str, QueryDict]
[docs]class BuildURL:
"""Tool to simplify the creation of URLs with query parameters.
Args:
base:
The base URL to build upon.
force_trailing_slash:
Whether or not to forcefully include a trailing slash at the end
of `path`, `False` by default.
Examples:
>>> from buildurl import BuildURL
>>> url = BuildURL("https://pypi.org")
>>> print(url.get)
https://pypi.org
>>> url = BuildURL("https://example.com/test",
... force_trailing_slash=True)
>>> print(url.get)
https://example.com/test/
"""
[docs] def __init__(self, base: str = "", force_trailing_slash: bool = False):
"""Initialize a new instance of BuildURL."""
purl = urlsplit(base)
# scheme://netloc/path;params?query#fragment
# There can be one `params` per `path` element, so it's included as
# part of `path`, and not isolated
self.scheme: str = purl.scheme
self.netloc: str = purl.netloc
self._path_list: PathList = list()
self.query_dict: QueryDict = dict()
self.fragment: str = purl.fragment
self.trailing_slash: bool = False
self.force_trailing_slash: bool = force_trailing_slash
path_str: str = purl.path
if path_str:
self.path = path_str
query_str: str = purl.query
if query_str:
self.query = query_str
[docs] def copy(self) -> "BuildURL":
"""Create a deep copy of itself.
Examples:
>>> url = BuildURL("https://example.com")
>>> url_copy = url.copy()
>>> url /= "test"
>>> print(url.get)
https://example.com/test
>>> print(url_copy.get)
https://example.com
"""
return deepcopy(self)
[docs] def set_force_trailing_slash(self, enabled: bool = True) -> "BuildURL":
"""Set the `force_trailing_slash` attribute.
Args:
enabled:
The new value for `force_trailing_slash`, default `True`.
Returns:
Reference to self.
Examples:
>>> url = BuildURL("https://example.com")
>>> url.set_force_trailing_slash().add_path("test")
BuildURL(base='https://example.com/test/', force_trailing_slash=True)
>>> url.set_force_trailing_slash(False)
BuildURL(base='https://example.com/test', force_trailing_slash=False)
"""
self.force_trailing_slash = enabled
return self
[docs] def add_path(self, *args: Path) -> "BuildURL":
"""Add to the path.
Args:
*args:
The paths to add.
Can be a string containing a single path, multiple paths
separated by `/`, or a list of single path strings.
Returns:
Reference to self.
Examples:
>>> url = BuildURL("https://example.com")
>>> url.add_path("test")
BuildURL(...)
>>> print(url.get)
https://example.com/test
>>> url.add_path(["more", "paths"]).add_path("/again/and/again/")
BuildURL(...)
>>> print(url.get)
https://example.com/test/more/paths/again/and/again/
>>> url = BuildURL("https://example.com")
>>> url.add_path("never", "stopping", "to/play", ["with", "paths"])
BuildURL(...)
>>> print(url.get)
https://example.com/never/stopping/to/play/with/paths
"""
path_list = list()
for path in args:
if isinstance(path, str):
path_list.extend(path.split("/"))
elif isinstance(path, list):
# TODO Convert some types to `str`, like `int` and `float`
if not all((isinstance(p, str) for p in path)):
raise AttributeError
path_list.extend(path)
else:
raise AttributeError
if len(path_list):
self.trailing_slash = path_list[-1] == ""
path_list = [p for p in path_list if p] # Remove empty strings
self._path_list.extend(path_list)
return self
[docs] def add_query(self, *args: Query, **kwargs) -> "BuildURL":
"""Add a query argument.
Args:
*args:
The query keys and values to add.
Can be a string containing the keys and values, like
`"key1=value1&key2=value2"`, or a dict, like
`{"key1": "value1", "key2": "value2"}`.
**kwargs:
Keyword arguments corresponding to key-value pairs.
Returns:
Reference to self.
Examples:
>>> url = BuildURL("https://example.com")
>>> url.add_query({"key": "value"})
BuildURL(...)
>>> print(url.get)
https://example.com?key=value
>>> url.add_query("another=query&more=stuff")
BuildURL(...)
>>> print(url.get)
https://example.com?key=value&another=query&more=stuff
>>> url.add_query(a="b").add_query("c=d", "e=f")
BuildURL(...)
>>> print(url.get)
https://example.com?key=value&another=query&more=stuff&a=b&c=d&e=f
"""
query_dict = dict()
for query in args:
if isinstance(query, str):
query_dict.update(parse_qs(query))
elif isinstance(query, dict):
query_dict.update(query)
else:
raise AttributeError
if kwargs:
query_dict.update(kwargs)
self.query_dict.update(query_dict)
return self
@property
def path(self) -> str:
"""Path string."""
path = "/".join(self._path_list)
if self.trailing_slash or self.force_trailing_slash:
path += "/"
return path
@path.setter
def path(self, path: Optional[Path]):
"""Replace current path."""
self._path_list = list()
if path is not None:
self.add_path(path)
@property
def query(self) -> str:
"""Query string."""
return urlencode(self.query_dict, doseq=True)
@query.setter
def query(self, query: Optional[Query]):
"""Replace current query."""
self.query_dict = dict()
if query is not None:
self.add_query(query)
@property
def parts(self) -> Tuple[str, ...]:
"""Tuple of necessary parts to construct the URL."""
return (
self.scheme,
self.netloc,
self.path,
self.query,
self.fragment,
)
@property
def get(self) -> str:
"""Get the generated URL."""
return urlunsplit(self.parts)
[docs] def __itruediv__(self, path: Path) -> "BuildURL":
"""Add new path part to the URL inplace.
Essentially a shortcut to ``add_path``.
Args:
path:
New path to add.
Returns:
Reference to self.
Examples:
>>> url = BuildURL("https://example.com")
>>> url /= "test"
>>> print(url.get)
https://example.com/test
>>> url /= ["more", "paths"]
>>> print(url.get)
https://example.com/test/more/paths
>>> url /= "/again/and/again/"
>>> print(url.get)
https://example.com/test/more/paths/again/and/again/
"""
self.add_path(path)
return self
[docs] def __truediv__(self, path: Path) -> "BuildURL":
"""Generate new URL with added path.
Equivalent to first copying the URL, then using ``add_path``.
Args:
path:
New path to add.
Returns:
New BuildURL instance.
Examples:
>>> url = BuildURL("https://example.com")
>>> new_url = url / "testing"
>>> print(url.get)
https://example.com
>>> print(new_url.get)
https://example.com/testing
"""
out = self.copy()
out /= path
return out
[docs] def __iadd__(self, query: Query) -> "BuildURL":
"""Add query arguments inplace.
Essentially a shortcut to ``add_query``.
Args:
query:
The query key and value to add.
Returns:
Reference to self.
Examples:
>>> url = BuildURL("https://example.com")
>>> url += {"key": "value"}
>>> print(url.get)
https://example.com?key=value
>>> url += "another=query&more=stuff"
>>> print(url.get)
https://example.com?key=value&another=query&more=stuff
"""
self.add_query(query)
return self
[docs] def __add__(self, query: Query) -> "BuildURL":
"""Generate new URL with added query.
Equivalent to first copying the URL, then using ``add_query``.
Args:
query:
The query key and value to add.
Returns:
New BuildURL instance.
Examples:
>>> url = BuildURL("https://example.com")
>>> new_url = url + {"test": "it"}
>>> print(url.get)
https://example.com
>>> print(new_url.get)
https://example.com?test=it
"""
out = self.copy()
out += query
return out
[docs] def __repr__(self) -> str:
"""Representation of the current instance.
Returns:
String representation of self.
Examples:
>>> url = BuildURL("https://example.com/test?now=true")
>>> print(repr(url))
BuildURL(base='https://example.com/test?now=true', force_trailing_slash=False)
"""
return f"{self.__class__.__name__}(base='{self.get}', force_trailing_slash={self.force_trailing_slash})"
[docs] def __str__(self) -> str:
"""Shortcut for getting the URL.
Can be obtained by printing the instance of the class.
Returns:
Generated URL.
Examples:
>>> url = BuildURL("https://example.com")
>>> url /= "test"
>>> print(str(url))
https://example.com/test
>>> print(url)
https://example.com/test
"""
return self.get
[docs] def __len__(self) -> int:
"""Length of the generated URL."""
return len(self.get)