Source code for jgdv.files.bookmarks.bookmark

  1#!/usr/bin/env python3
  2"""
  3
  4"""
  5# ruff: noqa: N805
  6# Imports:
  7from __future__ import annotations
  8
  9# ##-- stdlib imports
 10import datetime
 11import enum
 12import functools as ftz
 13import itertools as itz
 14import logging as logmod
 15import pathlib as pl
 16import re
 17import time
 18import types
 19import urllib
 20import weakref
 21from uuid import UUID, uuid1
 22
 23# ##-- end stdlib imports
 24
 25# ##-- types
 26# isort: off
 27import abc
 28import collections.abc
 29from typing import TYPE_CHECKING, cast, assert_type, assert_never
 30from typing import Generic, NewType
 31# Protocols:
 32from typing import Protocol, runtime_checkable
 33# Typing Decorators:
 34from typing import no_type_check, final, override, overload
 35from pydantic import BaseModel, Field, model_validator, field_validator, ValidationError
 36import urllib.parse
 37
 38if TYPE_CHECKING:
 39    from jgdv import Maybe, Rx
 40    from typing import Final
 41    from typing import ClassVar, Any, LiteralString
 42    from typing import Never, Self, Literal
 43    from typing import TypeGuard
 44    from collections.abc import Iterable, Iterator, Callable, Generator
 45    from collections.abc import Sequence, Mapping, MutableMapping, Hashable
 46
 47    from jgdv.files.tags import SubstitutionFile
 48
 49    type UrlParseResult = urllib.parse.ParseResult
 50
 51# isort: on
 52# ##-- end types
 53
 54##-- logging
 55logging = logmod.getLogger(__name__)
 56##-- end logging
 57
[docs] 58class Bookmark(BaseModel): 59 """A Single Bookmark in a collection.""" 60 url : str 61 tags : set[str] = set() 62 name : str = "No Name" 63 _tag_sep : ClassVar[str] = " : " 64 _tag_norm_re : ClassVar[Rx] = re.compile(" +") 65
[docs] 66 @classmethod 67 def build[T:Bookmark](cls:type[T], line:str, sep:Maybe[str]=None) -> T: 68 """ 69 Build a bookmark from a line of a bookmark file 70 """ 71 url : str 72 tags : list 73 sep = sep or Bookmark._tag_sep 74 tags = [] 75 match [x.strip() for x in line.split(sep)]: 76 case []: 77 msg = "Bad line passed to Bookmark" 78 raise TypeError(msg) 79 case [url]: 80 logging.warning("No Tags for: %s", url) 81 case [url, *tags]: 82 pass 83 84 return cls(url=url, 85 tags=set(tags))
86 87
[docs] 88 @field_validator("tags", mode="before") 89 def _validate_tags(cls, val:list|set|str) -> set: 90 match val: 91 case list()|set(): 92 return { Bookmark._tag_norm_re.sub("_", x.strip()) for x in val } 93 case str(): 94 return { Bookmark._tag_norm_re.sub("_", x.strip()) for x in val.split(Bookmark._tag_sep) } 95 case _: 96 msg = "Unrecognized tags base" 97 raise ValueError(msg, val)
98 99 @override 100 def __hash__(self) -> int: 101 return hash(self.url) 102 103 @override 104 def __eq__(self, other:object) -> bool: 105 match other: 106 case Bookmark() as o: 107 return self.url == o.url 108 case _: 109 return False 110 111 def __lt__(self, other:object) -> bool: 112 match other: 113 case Bookmark() as o: 114 return self.url < o.url 115 case _: 116 return False 117 118 @override 119 def __str__(self) -> str: 120 sep = Bookmark._tag_sep 121 tags = sep.join(sorted(self.tags)) 122 return f"{self.url}{sep}{tags}" 123
[docs] 124 @property 125 def url_comps(self) -> UrlParseResult: 126 return urllib.parse.urlparse(self.url)
127
[docs] 128 def merge(self, other:Bookmark) -> Bookmark: 129 """ Merge two bookmarks' tags together, 130 creating a new bookmark 131 """ 132 assert(self == other) 133 merged = Bookmark(url=self.url, 134 tags=self.tags.union(other.tags), 135 name=self.name) 136 return merged
137
[docs] 138 def clean(self, subs:SubstitutionFile) -> None: 139 """ 140 run tag substitutions on all tags in the bookmark 141 """ 142 cleaned_tags = set() 143 for tag in self.tags: 144 cleaned_tags.update(subs.sub(tag)) 145 146 self.tags = cleaned_tags