Coverage for src/bioimageio/spec/_internal/root_url.py: 68%

73 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 19:19 +0000

1from __future__ import annotations 

2 

3from abc import ABC 

4from pathlib import PurePosixPath 

5from typing import Any, ClassVar, Generic, Iterable, TypeVar 

6from urllib.parse import urlsplit, urlunsplit 

7 

8import pydantic 

9from pydantic import RootModel 

10 

11from .validated_string import ValidatedString 

12 

13ValidatedType = TypeVar("ValidatedType", pydantic.HttpUrl, pydantic.FtpUrl) 

14 

15 

16class _RootUrl(ValidatedString, ABC, Generic[ValidatedType]): 

17 """An untested HTTP URL, possibly a 'URL folder' or an invalid HTTP URL""" 

18 

19 root_model: ClassVar[type[RootModel[Any]]] 

20 _validated: ValidatedType 

21 

22 def absolute(self): 

23 """analog to `absolute` method of pathlib.""" 

24 return self 

25 

26 @property 

27 def scheme(self) -> str: 

28 return self._validated.scheme 

29 

30 @property 

31 def host(self) -> str | None: 

32 return self._validated.host 

33 

34 @property 

35 def path(self) -> str | None: 

36 return self._validated.path 

37 

38 @property 

39 def suffix(self) -> str: 

40 if self.path is None: 

41 return "" 

42 else: 

43 return PurePosixPath(self.path).suffix 

44 

45 @property 

46 def suffixes(self) -> list[str]: 

47 if self.path is None: 

48 return [] 

49 else: 

50 return PurePosixPath(self.path).suffixes 

51 

52 def __truediv__(self, other: str) -> RootHttpUrl: 

53 parsed = urlsplit(str(self)) 

54 return RootHttpUrl( 

55 urlunsplit( 

56 ( 

57 parsed.scheme, 

58 parsed.netloc, 

59 f"{parsed.path.strip('/')}/{other.strip('/')}", 

60 parsed.query, 

61 parsed.fragment, 

62 ) 

63 ) 

64 ) 

65 

66 

67class RootHttpUrl(_RootUrl[pydantic.HttpUrl]): 

68 """An untested HTTP URL, possibly a 'URL folder'""" 

69 

70 root_model: ClassVar[type[RootModel[Any]]] = RootModel[pydantic.HttpUrl] 

71 _validated: pydantic.HttpUrl 

72 

73 @property 

74 def parent(self) -> RootHttpUrl: 

75 parsed = urlsplit(str(self)) 

76 path = list(parsed.path.split("/")) 

77 if ( 

78 parsed.netloc == "zenodo.org" 

79 and parsed.path.startswith("/api/records/") 

80 and parsed.path.endswith("/content") 

81 ): 

82 path[-2:-1] = [] 

83 else: 

84 path = path[:-1] 

85 

86 return RootHttpUrl( 

87 urlunsplit( 

88 ( 

89 parsed.scheme, 

90 parsed.netloc, 

91 "/".join(path), 

92 parsed.query, 

93 parsed.fragment, 

94 ) 

95 ) 

96 ) 

97 

98 @property 

99 def parents(self) -> Iterable[RootHttpUrl]: 

100 """iterate over all URL parents (max 100)""" 

101 current = self 

102 for _ in range(100): 

103 parent = current.parent 

104 if current == parent: 

105 break 

106 

107 current = parent 

108 yield parent 

109 

110 

111class FtpUrl(_RootUrl[pydantic.FtpUrl]): 

112 """An untested FTP URL""" 

113 

114 root_model: ClassVar[type[RootModel[Any]]] = RootModel[pydantic.FtpUrl] 

115 _validated: pydantic.FtpUrl 

116 

117 @property 

118 def parent(self) -> FtpUrl: 

119 parsed = urlsplit(str(self)) 

120 path = list(parsed.path.split("/"))[:-1] 

121 return FtpUrl( 

122 urlunsplit( 

123 ( 

124 parsed.scheme, 

125 parsed.netloc, 

126 "/".join(path), 

127 parsed.query, 

128 parsed.fragment, 

129 ) 

130 ) 

131 ) 

132 

133 @property 

134 def parents(self) -> Iterable[FtpUrl]: 

135 """iterate over all URL parents (max 100)""" 

136 current = self 

137 for _ in range(100): 

138 parent = current.parent 

139 if current == parent: 

140 break 

141 

142 current = parent 

143 yield parent