Source code for buildurl.builder

"""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)