# -*- coding: utf-8 -*- from __future__ import annotations import pathlib import typing import urllib.parse class URL: """ The URL class. Example: .. code-block:: python url = URL('https://ziomek.dog/').\ set_path('/index_old.html').\ set_query({ 'test': 'true', }).\ to_string() The *encoding* argument will be used to decode *initial* if it's ```bytes``. Also, it'll be used when decoding QS. """ def __init__(self, initial: str | bytes | pathlib.Path | urllib.parse.ParseResult, encoding: str = 'utf-8'): self.encoding = encoding self.parsed_url: urllib.parse.ParseResult if isinstance(initial, urllib.parse.ParseResult): self.parsed_url = initial elif isinstance(initial, pathlib.Path): self.parsed_url = urllib.parse.urlparse(str(initial)) elif isinstance(initial, bytes): self.parsed_url = urllib.parse.urlparse( initial.decode(self.encoding), ) else: self.parsed_url = urllib.parse.urlparse(initial) def replace_parsed_url_field(self, field: str, value: typing.Any): self.parsed_url = self.parsed_url._replace(**{field: value}) return self def append_path_component(self, component: str | pathlib.Path) -> URL: _ = self.set_path(self.path / component) return self @property def scheme(self) -> str: return self.parsed_url.scheme def set_scheme(self, new_scheme: str) -> URL: self.replace_parsed_url_field('scheme', new_scheme) return self @property def netloc(self) -> str: return self.parsed_url.netloc def set_netloc(self, new_netloc: str) -> URL: self.replace_parsed_url_field('netloc', new_netloc) return self @property def hostname(self) -> str | None: return self.parsed_url.hostname @property def port(self) -> int | None: return self.parsed_url.port @property def username(self) -> str | None: return self.parsed_url.username @property def password(self) -> str | None: return self.parsed_url.password @property def path(self) -> pathlib.PurePosixPath: return pathlib.PurePosixPath(self.parsed_url.path) def set_path(self, new_path: str | pathlib.Path | pathlib.PurePath) -> URL: self.replace_parsed_url_field( 'path', str(pathlib.PurePosixPath(new_path)), ) return self @property def params(self) -> str: return self.parsed_url.params def set_params(self, new_params: str) -> URL: self.replace_parsed_url_field('params', new_params) return self @property def query(self) -> dict[typing.AnyStr, list[typing.AnyStr]]: if self.parsed_url.query == '': return {} return urllib.parse.parse_qs( self.parsed_url.query, encoding=self.encoding, # type: ignore[arg-type] ) def set_query(self, new_query: dict[typing.AnyStr, list[typing.AnyStr]] | list, ) -> URL: self.replace_parsed_url_field( 'query', urllib.parse.urlencode(new_query, doseq=True), ) return self @property def query_string(self) -> str: return self.parsed_url.query def set_query_string(self, new_query_string: str) -> URL: self.replace_parsed_url_field('query', new_query_string) return self @property def fragment(self) -> str: return self.parsed_url.fragment def set_fragment(self, new_fragment: str) -> URL: self.replace_parsed_url_field('fragment', new_fragment) return self def to_string(self) -> str: return urllib.parse.urlunparse(self.parsed_url) def __truediv__(self, component: str | pathlib.Path) -> URL: return self.append_path_component(component) def __mod__(self, query: dict[typing.AnyStr, list[typing.AnyStr]], ) -> URL: return self.set_query( {**self.query, **query}, ) def __str__(self) -> str: return self.to_string() def __repr__(self) -> str: return f'' def __bytes__(self) -> bytes: return self.__str__().encode(self.encoding)