Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/utils/__init__.py: 23%

101 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-13 11:04 +0000

1import datetime 

2import logging 

3import typing as t 

4import urllib.parse 

5import warnings 

6from collections.abc import Iterable 

7 

8from viur.core import current, db 

9from viur.core.config import conf 

10from deprecated.sphinx import deprecated 

11from . import json, parse, string # noqa: used by external imports 

12 

13if t.TYPE_CHECKING: 13 ↛ 14line 13 didn't jump to line 14 because the condition on line 13 was never true

14 from viur.core.skeleton import SkeletonInstance 

15 

16 

17def utcNow() -> datetime.datetime: 

18 """ 

19 Returns an actual timestamp with UTC timezone setting. 

20 """ 

21 return datetime.datetime.now(datetime.timezone.utc) 

22 

23 

24def seoUrlToEntry(module: str, 

25 entry: t.Optional["SkeletonInstance"] = None, 

26 skelType: t.Optional[str] = None, 

27 language: t.Optional[str] = None) -> str: 

28 """ 

29 Return the seo-url to a skeleton instance or the module. 

30 

31 :param module: The module name. 

32 :param entry: A skeleton instance or None, to get the path to the module. 

33 :param skelType: # FIXME: Not used 

34 :param language: For which language. 

35 If None, the language of the current request is used. 

36 :return: The path (with a leading /). 

37 """ 

38 from viur.core import conf 

39 pathComponents = [""] 

40 if language is None: 

41 language = current.language.get() 

42 if conf.i18n.language_method == "url": 

43 pathComponents.append(language) 

44 if module in conf.i18n.language_module_map and language in conf.i18n.language_module_map[module]: 

45 module = conf.i18n.language_module_map[module][language] 

46 pathComponents.append(module) 

47 if not entry: 

48 return "/".join(pathComponents) 

49 else: 

50 try: 

51 currentSeoKeys = entry["viurCurrentSeoKeys"] 

52 except: 

53 return "/".join(pathComponents) 

54 if language in (currentSeoKeys or {}): 

55 pathComponents.append(str(currentSeoKeys[language])) 

56 elif "key" in entry: 

57 key = entry["key"] 

58 if isinstance(key, str): 

59 try: 

60 key = db.Key.from_legacy_urlsafe(key) 

61 except: 

62 pass 

63 pathComponents.append(str(key.id_or_name) if isinstance(key, db.Key) else str(key)) 

64 elif "name" in dir(entry): 

65 pathComponents.append(str(entry.name)) 

66 return "/".join(pathComponents) 

67 

68 

69def seoUrlToFunction(module: str, function: str, render: t.Optional[str] = None) -> str: 

70 from viur.core import conf 

71 lang = current.language.get() 

72 if module in conf.i18n.language_module_map and lang in conf.i18n.language_module_map[module]: 

73 module = conf.i18n.language_module_map[module][lang] 

74 if conf.i18n.language_method == "url": 

75 pathComponents = ["", lang] 

76 else: 

77 pathComponents = [""] 

78 targetObject = conf.main_resolver 

79 if module in targetObject: 

80 pathComponents.append(module) 

81 targetObject = targetObject[module] 

82 if render and render in targetObject: 

83 pathComponents.append(render) 

84 targetObject = targetObject[render] 

85 if function in targetObject: 

86 func = targetObject[function] 

87 if func.seo_language_map and lang in func.seo_language_map: 

88 pathComponents.append(func.seo_language_map[lang]) 

89 else: 

90 pathComponents.append(function) 

91 return "/".join(pathComponents) 

92 

93 

94@deprecated(version="3.8.0", reason="Use 'db.normalize_key' instead") 

95def normalizeKey(key: t.Union[None, db.Key]) -> t.Union[None, db.Key]: 

96 """ 

97 Normalizes a datastore key (replacing _application with the current one) 

98 

99 :param key: Key to be normalized. 

100 

101 :return: Normalized key in string representation. 

102 """ 

103 db.normalize_key(key) 

104 

105 

106def ensure_iterable( 

107 obj: t.Any, 

108 *, 

109 test: t.Optional[t.Callable[[t.Any], bool]] = None, 

110 allow_callable: bool = True, 

111) -> t.Iterable[t.Any]: 

112 """ 

113 Ensures an object to be iterable. 

114 

115 An additional test can be provided to check additionally. 

116 

117 If the object is not considered to be iterable, a tuple with the object is returned. 

118 """ 

119 if allow_callable and callable(obj): 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true

120 obj = obj() 

121 

122 if not isinstance(obj, str) and isinstance(obj, Iterable): # uses collections.abc.Iterable 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true

123 if test is None or test(obj): 

124 return obj # return the obj, which is an iterable 

125 

126 return () # empty tuple 

127 

128 elif obj is None or (isinstance(obj, str) and not obj): 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true

129 return () # empty tuple 

130 

131 return obj, # return a tuple with the obj 

132 

133 

134def build_content_disposition_header( 

135 filename: str, 

136 *, 

137 attachment: bool = False, 

138 inline: bool = False, 

139) -> str: 

140 """ 

141 Build a Content-Disposition header with UTF-8 support and ASCII fallback. 

142 

143 Generates a properly formatted `Content-Disposition` header value, including 

144 both a fallback ASCII filename and a UTF-8 encoded filename using RFC 5987. 

145 

146 Set either `attachment` or `inline` to control content disposition type. 

147 If both are False, the header will omit disposition type (not recommended). 

148 

149 Example: 

150 filename = "Änderung.pdf" ➜ 

151 'attachment; filename="Anderung.pdf"; filename*=UTF-8\'\'%C3%84nderung.pdf' 

152 

153 :param filename: The desired filename for the content. 

154 :param attachment: Whether to mark the content as an attachment. 

155 :param inline: Whether to mark the content as inline. 

156 :return: A `Content-Disposition` header string. 

157 """ 

158 if attachment and inline: 

159 raise ValueError("Only one of 'attachment' or 'inline' may be True.") 

160 

161 fallback = string.normalize_ascii(filename) 

162 quoted_utf8 = urllib.parse.quote_from_bytes(filename.encode("utf-8")) 

163 

164 content_disposition = "; ".join( 

165 item for item in ( 

166 "attachment" if attachment else None, 

167 "inline" if inline else None, 

168 f'filename="{fallback}"' if filename else None, 

169 f'filename*=UTF-8\'\'{quoted_utf8}' if filename else None, 

170 ) if item 

171 ) 

172 

173 return content_disposition 

174 

175 

176# DEPRECATED ATTRIBUTES HANDLING 

177__UTILS_CONF_REPLACEMENT = { 

178 "projectID": "viur.instance.project_id", 

179 "isLocalDevelopmentServer": "viur.instance.is_dev_server", 

180 "projectBasePath": "viur.instance.project_base_path", 

181 "coreBasePath": "viur.instance.core_base_path" 

182} 

183 

184__UTILS_NAME_REPLACEMENT = { 

185 "currentLanguage": ("current.language", current.language), 

186 "currentRequest": ("current.request", current.request), 

187 "currentRequestData": ("current.request_data", current.request_data), 

188 "currentSession": ("current.session", current.session), 

189 "downloadUrlFor": ("conf.main_app.file.create_download_url", lambda: conf.main_app.file.create_download_url), 

190 "escapeString": ("utils.string.escape", string.escape), 

191 "generateRandomString": ("utils.string.random", string.random), 

192 "getCurrentUser": ("current.user.get", current.user.get), 

193 "is_prefix": ("utils.string.is_prefix", string.is_prefix), 

194 "parse_bool": ("utils.parse.bool", parse.bool), 

195 "srcSetFor": ("conf.main_app.file.create_src_set", lambda: conf.main_app.file.create_src_set), 

196} 

197 

198 

199def __getattr__(attr): 

200 if replace := __UTILS_CONF_REPLACEMENT.get(attr): 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true

201 msg = f"Use of `utils.{attr}` is deprecated; Use `conf.{replace}` instead!" 

202 warnings.warn(msg, DeprecationWarning, stacklevel=3) 

203 logging.warning(msg, stacklevel=3) 

204 return conf[replace] 

205 

206 if replace := __UTILS_NAME_REPLACEMENT.get(attr): 206 ↛ 207line 206 didn't jump to line 207 because the condition on line 206 was never true

207 msg = f"Use of `utils.{attr}` is deprecated; Use `{replace[0]}` instead!" 

208 warnings.warn(msg, DeprecationWarning, stacklevel=3) 

209 logging.warning(msg, stacklevel=3) 

210 res = replace[1] 

211 if isinstance(res, t.Callable): 

212 res = res() 

213 return res 

214 

215 return super(__import__(__name__).__class__).__getattribute__(attr)