from __future__ import annotations import copy from functools import cached_property from typing import TYPE_CHECKING, override if TYPE_CHECKING: import urllib.parse from typing import Self # Make sure URIs are interpreted indentically everywhere class Uri: def __assemble( self, scheme: bool, credentials: bool, secure: bool, path: bool ) -> str: ret = '' if self.__p.path.startswith('~'): return self.path if scheme: ret += f'{self.protocol}://' if credentials and self.username: ret += self.username if self.password: ret += ':' + '' if secure else self.password ret += '@' if self.hostname: ret += self.hostname if self.port_str: ret += ':' + self.port_str if path: ret += self.path return ret def __init__(self, string: str) -> None: self.__string = string self.__username: str | None = None self.__password: str | None = None @override def __repr__(self) -> str: return self.full @override def __str__(self) -> str: return self.safe_full_with_username @cached_property def __p(self) -> urllib.parse.ParseResult: from urllib.parse import urlparse return urlparse(self.__string) @classmethod def pimp(cls, url: str | Self) -> Uri: if isinstance(url, Uri): return url return Uri(url) @property def to_string(self) -> str: return self.__string @cached_property def scheme(self) -> str: ret = self.__p.scheme if not ret and not self.__p.path.startswith('~'): return 'file' return ret @cached_property def protocol(self) -> str: return self.scheme.replace('://', '') @property def username(self) -> str | None: if self.__username is None: return self.__p.username return self.__username def set_username(self, username: str) -> None: self.__username = username @property def password(self) -> str | None: if self.__password is None: return self.__p.password return self.__password def set_password(self, password: str) -> None: self.__password = password @cached_property def hostname(self) -> str | None: return self.__p.hostname @cached_property def port(self) -> int | None: return self.__p.port @cached_property def port_str(self) -> str | None: if self.port is None: return None return str(self.port) @cached_property def path(self) -> str: return self.__p.path @cached_property def basename(self) -> str: return self.__p.path.rsplit('/')[-1] @cached_property def authority(self) -> str: return self.__assemble( scheme = False, credentials = True, secure = False, path = False ) @cached_property def origin(self) -> str: return self.__assemble( scheme = False, credentials = False, secure = True, path = False ) @cached_property def scheme_plus_authority(self) -> str: return self.scheme + '://' + self.authority @cached_property def id(self) -> str: return self.__assemble( scheme = True, credentials = True, secure = True, path = False ) @cached_property def full(self) -> str: return self.__assemble( scheme = True, credentials = True, secure = False, path = True ) @cached_property def safe_full_with_username(self) -> str: return self.__assemble( scheme = True, credentials = True, secure = True, path = True ) def __new_with_path(self, base: str, path: str) -> Self: ret = copy.deepcopy(self) ret.__string = base ret.__username = None ret.__password = None if not path: return ret if ret.__string[-1] == '/': if path[0] == '/': ret.__string += path[1:] else: ret.__string += path else: if path[0] == '/': ret.__string += path else: ret.__string += '/' + path return ret def new_add_path(self, path: str) -> Self: return self.__new_with_path(self.__string, path) def new_replace_path(self, path: str) -> Self: return self.__new_with_path(self.scheme_plus_authority, path)