Coverage for app/venv/lib/python3.14/site-packages/weblate/utils/environment.py: 55%
119 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
1# Copyright © Michal Čihař <michal@weblate.org>
2#
3# SPDX-License-Identifier: GPL-3.0-or-later
5from __future__ import annotations
7import ast
8import os
9from typing import Any
10from urllib.parse import quote
12from django.core.exceptions import ImproperlyConfigured
15def get_env_str(
16 name: str,
17 default: str | None = None,
18 required: bool = False,
19 fallback_name: str | None = None,
20) -> str:
21 file_env = f"{name}_FILE"
22 if filename := os.environ.get(file_env): 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true
23 try:
24 with open(filename) as handle:
25 result = handle.read()
26 except OSError as error:
27 msg = f"Failed to open {filename} as specified by {file_env}: {error}"
28 raise ImproperlyConfigured(msg) from error
29 else:
30 if fallback_name and name not in os.environ: 30 ↛ 31line 30 didn't jump to line 31 because the condition on line 30 was never true
31 name = fallback_name
32 result = os.environ.get(
33 name,
34 default, # type: ignore[arg-type]
35 )
36 if required and not result: 36 ↛ 37line 36 didn't jump to line 37 because the condition on line 36 was never true
37 msg = f"{name} has to be configured!"
38 raise ImproperlyConfigured(msg)
39 return result
42def get_env_list(name: str, default: list[str] | None = None) -> list[str]:
43 """Get list from environment."""
44 if name not in os.environ:
45 return default or []
46 return os.environ[name].split(",")
49def get_env_map(name: str, default: dict[str, str] | None = None) -> dict[str, str]:
50 """
51 Get mapping from environment.
53 parses 'full_name:name,email:mail' into {'email': 'mail', 'full_name': 'name'}
54 """
55 if os.environ.get(name):
56 return dict(e.split(":") for e in os.environ[name].split(","))
57 return default or {}
60def get_env_int(name: str, default: int = 0) -> int:
61 """Get integer value from environment."""
62 if name not in os.environ:
63 return default
64 try:
65 return int(os.environ[name])
66 except ValueError as error:
67 msg = f"{name} is not an integer: {error}"
68 raise ImproperlyConfigured(msg) from error
71def get_env_float(name: str, default: float = 0.0) -> float:
72 """Get float value from environment."""
73 if name not in os.environ: 73 ↛ 75line 73 didn't jump to line 75 because the condition on line 73 was always true
74 return default
75 try:
76 return float(os.environ[name])
77 except ValueError as error:
78 msg = f"{name} is not an float: {error}"
79 raise ImproperlyConfigured(msg) from error
82def get_env_bool(name: str, default: bool = False) -> bool:
83 """Get boolean value from environment."""
84 if name not in os.environ:
85 return default
86 true_values = {"true", "yes", "1"}
87 return os.environ[name].lower() in true_values
90def modify_env_list(current: list[str], name: str) -> list[str]:
91 """Modify list based on environment (for example checks)."""
92 for item in reversed(get_env_list(f"WEBLATE_ADD_{name}")): 92 ↛ 93line 92 didn't jump to line 93 because the loop on line 92 never started
93 current.insert(0, item)
94 for item in get_env_list(f"WEBLATE_REMOVE_{name}"): 94 ↛ 95line 94 didn't jump to line 95 because the loop on line 94 never started
95 current.remove(item)
96 return current
99def get_env_credentials(
100 name: str,
101) -> dict[str, dict[str, str]]:
102 """Get VCS integration credentials from environment."""
103 if found_env_credentials := get_env_str(f"WEBLATE_{name}_CREDENTIALS"): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true
104 try:
105 return ast.literal_eval(found_env_credentials)
106 except ValueError as error:
107 msg = f"Could not parse {name}_CREDENTIALS: {error}"
108 raise ImproperlyConfigured(msg) from error
110 username = os.environ.get(f"WEBLATE_{name}_USERNAME", "")
111 token = os.environ.get(f"WEBLATE_{name}_TOKEN", "")
112 host = os.environ.get(f"WEBLATE_{name}_HOST")
113 organization = os.environ.get(f"WEBLATE_{name}_ORGANIZATION")
115 if not host: 115 ↛ 121line 115 didn't jump to line 121 because the condition on line 115 was always true
116 if username or token: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 msg = f"Incomplete {name}_CREDENTIALS configuration: missing WEBLATE_{name}_HOST"
118 raise ImproperlyConfigured(msg)
119 return {}
121 credentials = {host: {"username": username, "token": token}}
123 if organization is not None:
124 credentials[host]["organization"] = organization
126 return credentials
129def get_env_ratelimit(name: str, default: str) -> str:
130 value = os.environ.get(name, default)
132 # Taken from rest_framework.throttling.SimpleRateThrottle.parse_rate
133 # it can not be imported here as that breaks config loading for
134 # rest_framework
136 try:
137 num, period = value.split("/")
138 except ValueError as error:
139 msg = f"Could not parse {name}: {error}"
140 raise ImproperlyConfigured(msg) from error
141 if not num.isdigit(): 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true
142 msg = f"Could not parse {name}: rate is not numeric: {num}"
143 raise ImproperlyConfigured(msg)
144 if period[0] not in {"s", "m", "h", "d"}: 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true
145 msg = f"Could not parse {name}: unknown period: {period}"
146 raise ImproperlyConfigured(msg)
148 return value
151def url_quote_part(value: str) -> str:
152 return quote(value, safe="")
155def get_env_redis_url() -> str:
156 # Get values from the environment
157 redis_proto = "rediss" if get_env_bool("REDIS_TLS") else "redis"
158 redis_host = url_quote_part(get_env_str("REDIS_HOST", "cache", required=True))
159 redis_port = get_env_int("REDIS_PORT", 6379)
160 redis_db = get_env_int("REDIS_DB", 1)
161 redis_password = url_quote_part(get_env_str("REDIS_PASSWORD", ""))
162 redis_user = url_quote_part(get_env_str("REDIS_USER", ""))
164 # Build user/password part of the URL
165 redis_user_password: str | None
166 if redis_user and redis_password: 166 ↛ 167line 166 didn't jump to line 167 because the condition on line 166 was never true
167 redis_user_password = f"{redis_user}:{redis_password}@"
168 elif redis_password: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true
169 redis_user_password = f":{redis_password}@"
170 elif redis_user: 170 ↛ 171line 170 didn't jump to line 171 because the condition on line 170 was never true
171 redis_user_password = f"{redis_user}@"
172 else:
173 redis_user_password = ""
175 return f"{redis_proto}://{redis_user_password}{redis_host}:{redis_port}/{redis_db}"
178def get_saml_idp() -> dict[str, Any] | None:
179 idp_entity_id = get_env_str("WEBLATE_SAML_IDP_ENTITY_ID")
180 if not idp_entity_id: 180 ↛ 183line 180 didn't jump to line 183 because the condition on line 180 was always true
181 return None
182 # Identity Provider
183 saml_idp = {
184 "entity_id": idp_entity_id,
185 "url": get_env_str("WEBLATE_SAML_IDP_URL"),
186 "x509cert": get_env_str("WEBLATE_SAML_IDP_X509CERT"),
187 }
189 for field in (
190 "attr_full_name",
191 "attr_first_name",
192 "attr_last_name",
193 "attr_username",
194 "attr_email",
195 "attr_user_permanent_id",
196 ):
197 env_name = f"WEBLATE_SAML_ID_{field.upper()}"
198 value = get_env_str(env_name)
199 if value:
200 saml_idp[field] = value
202 return saml_idp