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
« 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
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
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
17def utcNow() -> datetime.datetime:
18 """
19 Returns an actual timestamp with UTC timezone setting.
20 """
21 return datetime.datetime.now(datetime.timezone.utc)
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.
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)
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)
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)
99 :param key: Key to be normalized.
101 :return: Normalized key in string representation.
102 """
103 db.normalize_key(key)
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.
115 An additional test can be provided to check additionally.
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()
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
126 return () # empty tuple
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
131 return obj, # return a tuple with the obj
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.
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.
146 Set either `attachment` or `inline` to control content disposition type.
147 If both are False, the header will omit disposition type (not recommended).
149 Example:
150 filename = "Änderung.pdf" ➜
151 'attachment; filename="Anderung.pdf"; filename*=UTF-8\'\'%C3%84nderung.pdf'
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.")
161 fallback = string.normalize_ascii(filename)
162 quoted_utf8 = urllib.parse.quote_from_bytes(filename.encode("utf-8"))
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 )
173 return content_disposition
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}
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}
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]
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
215 return super(__import__(__name__).__class__).__getattribute__(attr)