blob: af20367f531586d4c52a1c1371ebaa82fce1c914 [file] [log] [blame]
koder aka kdanilov652cd802015-04-13 12:21:07 +03001import re
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +03002import os
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +03003import sys
koder aka kdanilovffaf48d2016-12-27 02:25:29 +02004import math
koder aka kdanilov22d134e2016-11-08 11:33:19 +02005import time
6import uuid
koder aka kdanilovafd98742015-04-24 01:27:22 +03007import socket
koder aka kdanilove21d7472015-02-14 19:02:04 -08008import logging
koder aka kdanilovf2865172016-12-30 03:35:11 +02009import datetime
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030010import ipaddress
koder aka kdanilov7acd6bd2015-02-12 14:28:30 -080011import threading
12import contextlib
koder aka kdanilov652cd802015-04-13 12:21:07 +030013import subprocess
koder aka kdanilovf90de852017-01-20 18:12:27 +020014from fractions import Fraction
15
koder aka kdanilov4643fd62015-02-10 16:20:13 -080016
koder aka kdanilov108ac362017-01-19 20:17:16 +020017from typing import Any, Tuple, Union, List, Iterator, Iterable, Optional, IO, cast, TypeVar, Callable
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030018
koder aka kdanilovbb5fe072015-05-21 02:50:23 +030019try:
20 import psutil
21except ImportError:
22 psutil = None
23
koder aka kdanilov22d134e2016-11-08 11:33:19 +020024try:
25 from petname import Generate as pet_generate
26except ImportError:
27 def pet_generate(x: str, y: str) -> str:
28 return str(uuid.uuid4())
29
koder aka kdanilove21d7472015-02-14 19:02:04 -080030
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +030031logger = logging.getLogger("wally")
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020032TNumber = TypeVar('TNumber', int, float)
33Number = Union[int, float]
koder aka kdanilov209e85d2015-04-27 23:11:05 +030034
35
koder aka kdanilova732a602017-02-01 20:29:56 +020036STORAGE_ROLES = {'ceph-osd'}
37
38
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030039class StopTestError(RuntimeError):
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030040 pass
koder aka kdanilovf86d7af2015-05-06 04:01:54 +030041
42
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030043class LogError:
koder aka kdanilov22d134e2016-11-08 11:33:19 +020044 def __init__(self, message: str, exc_logger: logging.Logger = None) -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030045 self.message = message
46 self.exc_logger = exc_logger
47
koder aka kdanilov22d134e2016-11-08 11:33:19 +020048 def __enter__(self) -> 'LogError':
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030049 return self
50
koder aka kdanilov22d134e2016-11-08 11:33:19 +020051 def __exit__(self, tp: type, value: Exception, traceback: Any) -> bool:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030052 if value is None or isinstance(value, StopTestError):
koder aka kdanilov22d134e2016-11-08 11:33:19 +020053 return False
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030054
55 if self.exc_logger is None:
56 exc_logger = sys._getframe(1).f_globals.get('logger', logger)
57 else:
58 exc_logger = self.exc_logger
59
60 exc_logger.exception(self.message, exc_info=(tp, value, traceback))
koder aka kdanilov22d134e2016-11-08 11:33:19 +020061 raise StopTestError(self.message) from value
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030062
63
koder aka kdanilov22d134e2016-11-08 11:33:19 +020064class TaskFinished(Exception):
koder aka kdanilov2c473092015-03-29 17:12:13 +030065 pass
koder aka kdanilov4643fd62015-02-10 16:20:13 -080066
koder aka kdanilov2c473092015-03-29 17:12:13 +030067
koder aka kdanilovffaf48d2016-12-27 02:25:29 +020068class Timeout(Iterable[float]):
69 def __init__(self, timeout: int, message: str = None, min_tick: int = 1, no_exc: bool = False) -> None:
70 self.end_time = time.time() + timeout
71 self.message = message
72 self.min_tick = min_tick
73 self.prev_tick_at = time.time()
74 self.no_exc = no_exc
75
76 def tick(self) -> bool:
77 current_time = time.time()
78
79 if current_time > self.end_time:
80 if self.message:
81 msg = "Timeout: {}".format(self.message)
82 else:
83 msg = "Timeout"
84
85 if self.no_exc:
86 return False
87
88 raise TimeoutError(msg)
89
90 sleep_time = self.min_tick - (current_time - self.prev_tick_at)
91 if sleep_time > 0:
92 time.sleep(sleep_time)
93 self.prev_tick_at = time.time()
94 else:
95 self.prev_tick_at = current_time
96
97 return True
98
99 def __iter__(self) -> Iterator[float]:
100 return cast(Iterator[float], self)
101
102 def __next__(self) -> float:
103 if not self.tick():
104 raise StopIteration()
105 return self.end_time - time.time()
106
107
108def greater_digit_pos(val: Number) -> int:
109 return int(math.floor(math.log10(val))) + 1
110
111
112def round_digits(val: TNumber, num_digits: int = 3) -> TNumber:
113 pow = 10 ** (greater_digit_pos(val) - num_digits)
114 return type(val)(int(val / pow) * pow)
115
116
117def is_ip(data: str) -> bool:
118 try:
119 ipaddress.ip_address(data)
120 return True
121 except ValueError:
122 return False
123
124
125def log_block(message: str, exc_logger:logging.Logger = None) -> LogError:
126 logger.debug("Starts : " + message)
127 return LogError(message, exc_logger)
128
129
130def check_input_param(is_ok: bool, message: str) -> None:
131 if not is_ok:
132 logger.error(message)
133 raise StopTestError(message)
134
135
136def parse_creds(creds: str) -> Tuple[str, str, str]:
137 """Parse simple credentials format user[:passwd]@host"""
138 user, passwd_host = creds.split(":", 1)
139
140 if '@' not in passwd_host:
141 passwd, host = passwd_host, None
142 else:
143 passwd, host = passwd_host.rsplit('@', 1)
144
145 return user, passwd, host
146
147
koder aka kdanilov2c473092015-03-29 17:12:13 +0300148SMAP = dict(k=1024, m=1024 ** 2, g=1024 ** 3, t=1024 ** 4)
koder aka kdanilov8ad6e812015-03-22 14:42:18 +0200149
150
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300151def ssize2b(ssize: Union[str, int]) -> int:
koder aka kdanilov8ad6e812015-03-22 14:42:18 +0200152 try:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300153 if isinstance(ssize, int):
koder aka kdanilov63e9c5a2015-04-28 23:06:07 +0300154 return ssize
koder aka kdanilov8ad6e812015-03-22 14:42:18 +0200155
koder aka kdanilov63e9c5a2015-04-28 23:06:07 +0300156 ssize = ssize.lower()
koder aka kdanilov2c473092015-03-29 17:12:13 +0300157 if ssize[-1] in SMAP:
158 return int(ssize[:-1]) * SMAP[ssize[-1]]
koder aka kdanilov8ad6e812015-03-22 14:42:18 +0200159 return int(ssize)
160 except (ValueError, TypeError, AttributeError):
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300161 raise ValueError("Unknow size format {!r}".format(ssize))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300162
163
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300164RSMAP = [('K', 1024),
165 ('M', 1024 ** 2),
166 ('G', 1024 ** 3),
167 ('T', 1024 ** 4)]
168
169
koder aka kdanilova732a602017-02-01 20:29:56 +0200170def b2ssize(value: Union[int, float]) -> str:
171 if isinstance(value, float) and value < 100:
172 return b2ssize_10(value)
173
174 value = int(value)
175 if value < 1024:
176 return str(value) + " "
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300177
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200178 # make mypy happy
179 scale = 1
180 name = ""
181
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300182 for name, scale in RSMAP:
koder aka kdanilova732a602017-02-01 20:29:56 +0200183 if value < 1024 * scale:
184 if value % scale == 0:
185 return "{} {}i".format(value // scale, name)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300186 else:
koder aka kdanilova732a602017-02-01 20:29:56 +0200187 return "{:.1f} {}i".format(float(value) / scale, name)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300188
koder aka kdanilova732a602017-02-01 20:29:56 +0200189 return "{}{}i".format(value // scale, name)
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300190
191
koder aka kdanilova732a602017-02-01 20:29:56 +0200192RSMAP_10 = [(' f', 0.001 ** 4),
193 (' n', 0.001 ** 3),
194 (' u', 0.001 ** 2),
195 (' m', 0.001),
196 (' ', 1),
197 (' K', 1000),
198 (' M', 1000 ** 2),
199 (' G', 1000 ** 3),
200 (' T', 1000 ** 4),
201 (' P', 1000 ** 5),
202 (' E', 1000 ** 6)]
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300203
204
koder aka kdanilova732a602017-02-01 20:29:56 +0200205def has_next_digit_after_coma(x: float) -> bool:
206 return x * 10 - int(x * 10) > 1
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300207
koder aka kdanilova732a602017-02-01 20:29:56 +0200208
209def has_second_digit_after_coma(x: float) -> bool:
210 return (x * 10 - int(x * 10)) * 10 > 1
211
212
213def b2ssize_10(value: Union[int, float]) -> str:
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200214 # make mypy happy
215 scale = 1
koder aka kdanilova732a602017-02-01 20:29:56 +0200216 name = " "
217
218 if value == 0.0:
219 return "0 "
220
221 if value / RSMAP_10[0][1] < 1.0:
222 return "{:.2e} ".format(value)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200223
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300224 for name, scale in RSMAP_10:
koder aka kdanilova732a602017-02-01 20:29:56 +0200225 cval = value / scale
226 if cval < 1000:
227 # detect how many digits after dot to show
228 if cval > 100:
229 return "{}{}".format(int(cval), name)
230 if cval > 10:
231 if has_next_digit_after_coma(cval):
232 return "{:.1f}{}".format(cval, name)
233 else:
234 return "{}{}".format(int(cval), name)
235 if cval >= 1:
236 if has_second_digit_after_coma(cval):
237 return "{:.2f}{}".format(cval, name)
238 elif has_next_digit_after_coma(cval):
239 return "{:.1f}{}".format(cval, name)
240 return "{}{}".format(int(cval), name)
241 raise AssertionError("Can't get here")
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300242
koder aka kdanilova732a602017-02-01 20:29:56 +0200243 return "{}{}".format(int(value // scale), name)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300244
245
koder aka kdanilova732a602017-02-01 20:29:56 +0200246def run_locally(cmd: Union[str, List[str]], input_data: str = "", timeout: int = 20) -> str:
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200247 if isinstance(cmd, str):
248 shell = True
249 cmd_str = cmd
250 else:
koder aka kdanilov73084622016-11-16 21:51:08 +0200251 shell = False
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200252 cmd_str = " ".join(cmd)
253
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300254 proc = subprocess.Popen(cmd,
255 shell=shell,
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300256 stdin=subprocess.PIPE,
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300257 stdout=subprocess.PIPE,
258 stderr=subprocess.PIPE)
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200259 res = [] # type: List[Tuple[bytes, bytes]]
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300260
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300261 def thread_func() -> None:
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200262 rr = proc.communicate(input_data.encode("utf8"))
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300263 res.extend(rr)
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300264
koder aka kdanilovfd2cfa52015-05-20 03:17:42 +0300265 thread = threading.Thread(target=thread_func,
266 name="Local cmd execution")
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300267 thread.daemon = True
268 thread.start()
269 thread.join(timeout)
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300270
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300271 if thread.is_alive():
koder aka kdanilovbb5fe072015-05-21 02:50:23 +0300272 if psutil is not None:
273 parent = psutil.Process(proc.pid)
274 for child in parent.children(recursive=True):
275 child.kill()
276 parent.kill()
277 else:
278 proc.kill()
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300279
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300280 thread.join()
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200281 raise RuntimeError("Local process timeout: " + cmd_str)
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300282
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200283 stdout_data, stderr_data = zip(*res) # type: List[bytes], List[bytes]
284
285 out = b"".join(stdout_data).decode("utf8")
286 err = b"".join(stderr_data).decode("utf8")
287
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300288 if 0 != proc.returncode:
koder aka kdanilov416b87a2015-05-12 00:26:04 +0300289 raise subprocess.CalledProcessError(proc.returncode,
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200290 cmd_str, out + err)
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300291
292 return out
293
294
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300295def get_ip_for_target(target_ip: str) -> str:
koder aka kdanilov209e85d2015-04-27 23:11:05 +0300296 if not is_ip(target_ip):
koder aka kdanilovafd98742015-04-24 01:27:22 +0300297 target_ip = socket.gethostbyname(target_ip)
298
koder aka kdanilov209e85d2015-04-27 23:11:05 +0300299 first_dig = map(int, target_ip.split("."))
300 if first_dig == 127:
koder aka kdanilovafd98742015-04-24 01:27:22 +0300301 return '127.0.0.1'
302
koder aka kdanilovd5ed4da2015-05-07 23:33:23 +0300303 data = run_locally('ip route get to'.split(" ") + [target_ip])
koder aka kdanilov652cd802015-04-13 12:21:07 +0300304
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300305 rr1 = r'{0} via [.0-9]+ dev (?P<dev>.*?) src (?P<ip>[.0-9]+)$'
306 rr1 = rr1.replace(" ", r'\s+')
307 rr1 = rr1.format(target_ip.replace('.', r'\.'))
308
309 rr2 = r'{0} dev (?P<dev>.*?) src (?P<ip>[.0-9]+)$'
310 rr2 = rr2.replace(" ", r'\s+')
311 rr2 = rr2.format(target_ip.replace('.', r'\.'))
koder aka kdanilov652cd802015-04-13 12:21:07 +0300312
313 data_line = data.split("\n")[0].strip()
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300314 res1 = re.match(rr1, data_line)
315 res2 = re.match(rr2, data_line)
koder aka kdanilov652cd802015-04-13 12:21:07 +0300316
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300317 if res1 is not None:
318 return res1.group('ip')
koder aka kdanilov652cd802015-04-13 12:21:07 +0300319
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300320 if res2 is not None:
321 return res2.group('ip')
322
323 raise OSError("Can't define interface for {0}".format(target_ip))
324
325
koder aka kdanilov39e449e2016-12-17 15:15:26 +0200326def open_for_append_or_create(fname: str) -> IO[str]:
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300327 if not os.path.exists(fname):
328 return open(fname, "w")
329
330 fd = open(fname, 'r+')
331 fd.seek(0, os.SEEK_END)
332 return fd
333
334
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300335def sec_to_str(seconds: int) -> str:
koder aka kdanilovcff7b2e2015-04-18 20:48:15 +0300336 h = seconds // 3600
337 m = (seconds % 3600) // 60
338 s = seconds % 60
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300339 return "{}:{:02d}:{:02d}".format(h, m, s)
koder aka kdanilov168f6092015-04-19 02:33:38 +0300340
341
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300342def yamable(data: Any) -> Any:
koder aka kdanilov168f6092015-04-19 02:33:38 +0300343 if isinstance(data, (tuple, list)):
344 return map(yamable, data)
345
koder aka kdanilov168f6092015-04-19 02:33:38 +0300346 if isinstance(data, dict):
347 res = {}
348 for k, v in data.items():
349 res[yamable(k)] = yamable(v)
350 return res
351
352 return data
koder aka kdanilovf86d7af2015-05-06 04:01:54 +0300353
354
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300355def flatten(data: Iterable[Any]) -> List[Any]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300356 res = []
357 for i in data:
358 if isinstance(i, (list, tuple, set)):
359 res.extend(flatten(i))
360 else:
361 res.append(i)
362 return res
koder aka kdanilov89fb6102015-06-13 02:58:08 +0300363
364
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200365def get_creds_openrc(path: str) -> Tuple[str, str, str, str, bool]:
koder aka kdanilov89fb6102015-06-13 02:58:08 +0300366 fc = open(path).read()
367
koder aka kdanilovb7197432015-07-15 00:40:43 +0300368 echo = 'echo "$OS_INSECURE:$OS_TENANT_NAME:$OS_USERNAME:$OS_PASSWORD@$OS_AUTH_URL"'
koder aka kdanilov89fb6102015-06-13 02:58:08 +0300369
370 msg = "Failed to get creads from openrc file"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300371 with LogError(msg):
koder aka kdanilov89fb6102015-06-13 02:58:08 +0300372 data = run_locally(['/bin/bash'], input_data=fc + "\n" + echo)
373
374 msg = "Failed to get creads from openrc file: " + data
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300375 with LogError(msg):
koder aka kdanilov89fb6102015-06-13 02:58:08 +0300376 data = data.strip()
koder aka kdanilovb7197432015-07-15 00:40:43 +0300377 insecure_str, user, tenant, passwd_auth_url = data.split(':', 3)
378 insecure = (insecure_str in ('1', 'True', 'true'))
koder aka kdanilov89fb6102015-06-13 02:58:08 +0300379 passwd, auth_url = passwd_auth_url.rsplit("@", 1)
380 assert (auth_url.startswith("https://") or
381 auth_url.startswith("http://"))
382
koder aka kdanilovb7197432015-07-15 00:40:43 +0300383 return user, passwd, tenant, auth_url, insecure
koder aka kdanilov89fb6102015-06-13 02:58:08 +0300384
385
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300386def which(program: str) -> Optional[str]:
koder aka kdanilova94dfe12015-08-19 13:04:51 +0300387 def is_exe(fpath):
388 return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
389
390 for path in os.environ["PATH"].split(os.pathsep):
391 path = path.strip('"')
392 exe_file = os.path.join(path, program)
393 if is_exe(exe_file):
394 return exe_file
395
396 return None
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200397
398
koder aka kdanilov108ac362017-01-19 20:17:16 +0200399@contextlib.contextmanager
400def empty_ctx(val: Any = None) -> Iterator[Any]:
401 yield val
402
403
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200404def get_uniq_path_uuid(path: str, max_iter: int = 10) -> Tuple[str, str]:
405 for i in range(max_iter):
406 run_uuid = pet_generate(2, "_")
407 results_dir = os.path.join(path, run_uuid)
408 if not os.path.exists(results_dir):
409 break
410 else:
411 run_uuid = str(uuid.uuid4())
412 results_dir = os.path.join(path, run_uuid)
413
414 return results_dir, run_uuid
415
416
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200417def to_ip(host_or_ip: str) -> str:
418 # translate hostname to address
419 try:
420 ipaddress.ip_address(host_or_ip)
421 return host_or_ip
422 except ValueError:
423 ip_addr = socket.gethostbyname(host_or_ip)
424 logger.info("Will use ip_addr %r instead of hostname %r", ip_addr, host_or_ip)
425 return ip_addr
koder aka kdanilovf2865172016-12-30 03:35:11 +0200426
427
428def get_time_interval_printable_info(seconds: int) -> Tuple[str, str]:
429 exec_time_s = sec_to_str(seconds)
430 now_dt = datetime.datetime.now()
431 end_dt = now_dt + datetime.timedelta(0, seconds)
432 return exec_time_s, "{:%H:%M:%S}".format(end_dt)
koder aka kdanilov108ac362017-01-19 20:17:16 +0200433
434
435FM_FUNC_INPUT = TypeVar("FM_FUNC_INPUT")
436FM_FUNC_RES = TypeVar("FM_FUNC_RES")
437
438
439def flatmap(func: Callable[[FM_FUNC_INPUT], Iterable[FM_FUNC_RES]],
440 inp_iter: Iterable[FM_FUNC_INPUT]) -> Iterator[FM_FUNC_RES]:
441 for val in inp_iter:
442 for res in func(val):
443 yield res
444
445
koder aka kdanilovf90de852017-01-20 18:12:27 +0200446_coefs = {
447 'n': Fraction(1, 1000**3),
448 'u': Fraction(1, 1000**2),
449 'm': Fraction(1, 1000),
450 'K': 1000,
451 'M': 1000 ** 2,
452 'G': 1000 ** 3,
453 'Ki': 1024,
454 'Mi': 1024 ** 2,
455 'Gi': 1024 ** 3,
456}
457
458
459def split_unit(units: str) -> Tuple[Union[Fraction, int], str]:
460 if len(units) > 2 and units[:2] in _coefs:
461 return _coefs[units[:2]], units[2:]
462 if len(units) > 1 and units[0] in _coefs:
463 return _coefs[units[0]], units[1:]
464 else:
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300465 return 1, units
koder aka kdanilovf90de852017-01-20 18:12:27 +0200466
467
kdanylov aka koder45183182017-04-30 23:55:40 +0300468conversion_cache = {}
469
470
koder aka kdanilovf90de852017-01-20 18:12:27 +0200471def unit_conversion_coef(from_unit: str, to_unit: str) -> Union[Fraction, int]:
kdanylov aka koder45183182017-04-30 23:55:40 +0300472 key = (from_unit, to_unit)
473 if key in conversion_cache:
474 return conversion_cache[key]
475
koder aka kdanilovf90de852017-01-20 18:12:27 +0200476 f1, u1 = split_unit(from_unit)
477 f2, u2 = split_unit(to_unit)
478
479 assert u1 == u2, "Can't convert {!r} to {!r}".format(from_unit, to_unit)
480
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300481 if isinstance(f1, int) and isinstance(f2, int):
482 if f1 % f2 != 0:
kdanylov aka koder45183182017-04-30 23:55:40 +0300483 res = Fraction(f1, f2)
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300484 else:
kdanylov aka koder45183182017-04-30 23:55:40 +0300485 res = f1 // f2
486 else:
487 res = f1 / f2
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300488
489 if isinstance(res, Fraction) and cast(Fraction, res).denominator == 1:
kdanylov aka koder45183182017-04-30 23:55:40 +0300490 res = cast(Fraction, res).numerator
491
492 conversion_cache[key] = res
kdanylov aka kodercdfcdaf2017-04-29 10:03:39 +0300493
494 return res
koder aka kdanilova732a602017-02-01 20:29:56 +0200495
496
497def shape2str(shape: Iterable[int]) -> str:
498 return "*".join(map(str, shape))
499
500
501def str2shape(shape: str) -> Tuple[int, ...]:
502 return tuple(map(int, shape.split('*')))