Source code for domonic.webapi.url

"""
    domonic.webapi.url
    ====================================
    https://developer.mozilla.org/en-US/docs/Web/API/URL

    # TODO - move the unit tests for this class from javascript to webapi
    # TODO - untested

"""
from __future__ import annotations

import urllib.parse
from typing import Any, Callable, Iterable, Iterator

ParamInput = str | dict[str, Any] | Iterable[tuple[str, Any]]


[docs] class URL: """a-tag extends from URL""" def __update__(self) -> None: # print( "update URL:", type(self), self ) try: new = { "protocol": self.protocol, "hostname": self.hostname, "href": self.href, "port": self.port, "host": self.host, "pathname": self.pathname, "hash": self.hash, "search": self.search, "_searchParams": self._searchParams, } # NOTE - rebuild happening here query = new["search"] or "" if query and not query.startswith("?"): query = "?" + query self.url = urllib.parse.urlsplit( new["protocol"] + "://" + new["host"] + new["pathname"] + query + new["hash"] ) self.href = self.url.geturl() except Exception: # as e: # print('fails on props called by init as they dont exist yet') # print(e) pass def __init__(self, url: str = "", *args: Any, **kwargs: Any) -> None: # TODO - relative to """URL builds a url Args: url (str): a url """ self.url = urllib.parse.urlsplit(url) self.href = url # self.url.geturl() self.protocol = self.url.scheme self.hostname = self.url.hostname self.port = self.url.port self.host = self.url.hostname self.pathname = self.url.path self.hash = "" self.search = self.url.query self._searchParams = URLSearchParams(self.url.query) @property def searchParams(self) -> str: return self._searchParams.toString() @property def origin(self) -> str: if not self.protocol or not self.host: return "" return f"{self.protocol}://{self.host}" def toString(self) -> str: return str(self.href) # def toJson # @property # def href(self): # TODO - check js vs tag. does js version remove query?. if so detect self. # return self.href # @href.setter # def href(self, href:str): # self.url = href # self.href = href @property def protocol(self) -> str: return self.__protocol @protocol.setter def protocol(self, p: str): self.__protocol = p # if self.ready : self.__update__() # TODO - this instead of silent err? self.__update__() @property def hostname(self) -> str | None: return self.__hostname @hostname.setter def hostname(self, h: str): if h is None: return if ":" in h: h = h.split(":")[0] self.__hostname = h self.__update__() @property def port(self) -> int | None: return self.__port @port.setter def port(self, p: int | None): self.__port = p self.__update__() @property def host(self) -> str | None: if self.hostname is None: return None if self.port is not None: return self.hostname + ":" + str(self.port) else: return self.hostname @host.setter def host(self, h: str): if h is None: return p = self.port if ":" in h: p = int(h.split(":")[1]) h = h.split(":")[0] self.__host = h self.hostname = h self.port = p self.__update__() @property def pathname(self) -> str: return self.__pathname @pathname.setter def pathname(self, p: str): self.__pathname = p self.__update__() @property def search(self) -> str: if not self.__search: return "" return self.__search if self.__search.startswith("?") else "?" + self.__search @search.setter def search(self, value: str): if value is None: value = "" self.__search = value if value == "" else value.lstrip("?") self._searchParams = URLSearchParams(self.__search) self.__update__() @property def hash(self) -> str: """ " hash Sets or returns the anchor part (#) of a URL""" if "#" in self.href: return "#" + self.href.split("#")[1] # return '' return self.__hash @hash.setter def hash(self, h: str): self.__hash = h self.__update__() # @property # def origin(self): """# origin Returns the protocol, hostname and port number of a URL Location""" def __str__(self) -> str: return str(self.href)
# NOTE - node -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # @staticmethod # def domainToASCII(domain: str): # """[It returns the Punycode ASCII serialization of the domain. # If domain is an invalid domain, the empty string is returned.] # Args: # domain (str): [description] # """ # pass # @staticmethod # def domainToUnicode(domain: str): # """[returns the Unicode serialization of the domain. # If the domain is invalid, the empty string is returned] # Args: # domain (str): [description] # """ # pass # @staticmethod # def fileURLToPath(url: str): # """[ensures the correct decodings of percent-encoded characters as well as # ensuring a cross-platform valid absolute path string.] # Args: # url (str): [The fully-resolved platform-specific file path.] # """ # if url is None: # return # return urllib.parse.unquote(url) # @staticmethod # def format(URL, options): # """[summary] # Args: # URL ([type]): [description] # options ([type]): [description] # """ # pass # @staticmethod # def pathToFileURL(path: str): # """[summary] # Args: # path (str): [description] # """ # pass # @staticmethod # def urlToHttpOptions(url: str): # """[summary] # Args: # url (str): [description] # """ # pass
[docs] class URLSearchParams: """[utility methods to work with the query string of a URL]""" def __init__(self, paramString: ParamInput): # , **paramsObj): """[Returns a URLSearchParams object instance.] Args: paramString ([type]): [ i.e. q=URLUtils.searchParams&topic=api] """ # TODO - escape # import ast # TODO - dont think i can do this cant urls params have duplicate keys? # self.params = ast.literal_eval(paramString) if isinstance(paramString, str): if paramString.startswith("?"): paramString = paramString[1 : len(paramString)] import urllib self.params: dict[str, list[str]] = urllib.parse.parse_qs(paramString) elif isinstance(paramString, dict): self.params = { str(key): value if isinstance(value, list) else [str(value)] for key, value in paramString.items() } elif hasattr(paramString, "__iter__"): self.params = {} for key, value in paramString: self.params.setdefault(str(key), []).append(str(value)) else: raise TypeError( f"Malformed paramString. Must be a string or a dict with dict like items. Got: {paramString}" ) def __iter__(self) -> Iterator[tuple[str, list[str]]]: for attr in self.params.items(): # dir(self.params.items()): # if not attr.startswith("__"): yield attr
[docs] def append(self, key: str, value: str) -> None: """Appends a specified key/value pair as a new search parameter""" # TODO - ordereddict? self.params.setdefault(key, []).append(value) # [key]=value
[docs] def delete(self, key: str) -> None: """Deletes the given search parameter, and its associated value, from the list of all search parameters.""" del self.params[key]
[docs] def has(self, key: str) -> bool: """Returns a Boolean indicating if such a given parameter exists.""" return key in self.params
[docs] def entries(self) -> Iterable[tuple[str, list[str]]]: """Returns an iterator allowing iteration through all key/value pairs contained in this object.""" return self.params.items()
[docs] def forEach(self, func: Callable[[str, list[str]], Any]) -> None: """Allows iteration through all values contained in this object via a callback function.""" for key, value in self.params.items(): func(key, value)
[docs] def keys(self) -> Iterable[str]: """Returns an iterator allowing iteration through all keys of the key/value pairs contained in this object.""" return self.params.keys()
[docs] def get(self, key: str) -> str | None: """Returns the first value associated with the given search parameter.""" try: return self.params.get(key, None)[0] except Exception: return None
[docs] def sort(self) -> None: """Sorts all key/value pairs, if any, by their keys.""" self.params = dict(sorted(self.params.items()))
[docs] def values(self) -> Iterable[list[str]]: """Returns an iterator allowing iteration through all values of the key/value pairs contained in this object.""" return self.params.values()
[docs] def toString(self) -> str: """Returns a string containing a query string suitable for use in a URL.""" # return '&'.join([str(x) for x in self.params]) return urllib.parse.urlencode(self.params, doseq=True)
# return str(self.params)
[docs] def set(self, key: str, value: str) -> None: """Sets the value associated with a given search parameter to the given value. If there are several values, the others are deleted.""" self.params[key] = [value]
[docs] def getAll(self, key: str) -> list[str] | None: """Returns all the values associated with a given search parameter.""" return self.params.get(key)
def __str__(self) -> str: return urllib.parse.urlencode(self.params, doseq=True)