Coverage for  / home / ubuntu / lunchbox / python / lunchbox / tools.py: 100%

278 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-26 04:02 +0000

1from typing import Any, Callable, Dict, Generator, List, Optional, Union # noqa: F401 

2 

3from itertools import dropwhile, takewhile 

4from pathlib import Path 

5from pprint import pformat 

6import inspect 

7import json 

8import logging 

9import os 

10import re 

11import urllib.request 

12 

13import wrapt 

14 

15from lunchbox.enforce import Enforce, EnforceError 

16from lunchbox.stopwatch import StopWatch 

17 

18LOG_LEVEL = os.environ.get('LOG_LEVEL', 'WARNING').upper() 

19logging.basicConfig(level=LOG_LEVEL) 

20LOGGER = logging.getLogger(__name__) 

21 

22FilePath = Path | str 

23# ------------------------------------------------------------------------------ 

24 

25 

26''' 

27A library of miscellaneous tools. 

28''' 

29 

30 

31def to_snakecase(string): 

32 # type: (str) -> str 

33 ''' 

34 Converts a given string to snake_case. 

35 

36 Args: 

37 string (str): String to be converted. 

38 

39 Returns: 

40 str: snake_case string. 

41 ''' 

42 output = re.sub('([A-Z]+)', r'_\1', string) 

43 output = re.sub('-', '_', output) 

44 output = re.sub(r'\.', '_', output) 

45 output = re.sub(' ', '_', output) 

46 output = re.sub('_+', '_', output) 

47 output = re.sub('^_|_$', '', output) 

48 output = output.lower() 

49 return output 

50 

51 

52def try_(function, item, return_item='item'): 

53 # type: (Callable[[Any], Any], Any, Any) -> Any 

54 ''' 

55 Call given function on given item, catch any exceptions and return given 

56 return item. 

57 

58 Args: 

59 function (function): Function of signature lambda x: x. 

60 item (object): Item used to call function. 

61 return_item (object, optional): Item to be returned. Default: "item". 

62 

63 Returns: 

64 object: Original item if return_item is "item". 

65 Exception: If return_item is "error". 

66 object: Object return by function call if return_item is not "item" or 

67 "error". 

68 ''' 

69 try: 

70 return function(item) 

71 except Exception as error: 

72 if return_item == 'item': 

73 return item 

74 elif return_item == 'error': 

75 return error 

76 return return_item 

77 

78 

79def get_ordered_unique(items): 

80 # type: (List) -> List 

81 ''' 

82 Generates a unique list of items in same order they were received in. 

83 

84 Args: 

85 items (list): List of items. 

86 

87 Returns: 

88 list: Unique ordered list. 

89 ''' 

90 output = [] 

91 temp = set() 

92 for item in items: 

93 if item not in temp: 

94 output.append(item) 

95 temp.add(item) 

96 return output 

97 

98 

99def relative_path(module, path): 

100 # type: (Union[str, Path], Union[str, Path]) -> Path 

101 ''' 

102 Resolve path given current module's file path and given suffix. 

103 

104 Args: 

105 module (str or Path): Always __file__ of current module. 

106 path (str or Path): Path relative to __file__. 

107 

108 Returns: 

109 Path: Resolved Path object. 

110 ''' 

111 module_root = Path(module).parent 

112 path_ = Path(path).parts # type: Any 

113 path_ = list(dropwhile(lambda x: x == ".", path_)) 

114 up = len(list(takewhile(lambda x: x == "..", path_))) 

115 path_ = Path(*path_[up:]) 

116 root = list(module_root.parents)[up - 1] 

117 output = Path(root, path_).absolute() 

118 

119 LOGGER.debug( 

120 f'relative_path called with: {module} and {path_}. Returned: {output}') 

121 return output 

122 

123 

124def truncate_list(items, size=3): 

125 # type (list, int) -> list 

126 ''' 

127 Truncates a given list to a given size, replaces the middle contents with 

128 "...". 

129 

130 Args: 

131 items (list): List of objects. 

132 size (int, optional): Size of output list. 

133 

134 Raises: 

135 EnforceError: If item is not a list. 

136 EnforceError: If size is not an integer greater than -1. 

137 

138 Returns: 

139 list: List of given size. 

140 ''' 

141 Enforce(items, 'instance of', list, message='Items must be a list.') 

142 msg = 'Size must be an integer greater than -1. Given value: {a}.' 

143 Enforce(size, 'instance of', int, message=msg) 

144 Enforce(size, '>', -1, message=msg) 

145 # -------------------------------------------------------------------------- 

146 

147 if len(items) <= size: 

148 return items 

149 if size == 0: 

150 return [] 

151 if size == 1: 

152 return items[:1] 

153 if size == 2: 

154 return [items[0], items[-1]] 

155 

156 output = items[:size - 2] 

157 output.append('...') 

158 output.append(items[-1]) 

159 return output 

160 

161 

162def truncate_blob_lists(blob, size=3): 

163 # type: (dict, int) -> dict 

164 ''' 

165 Truncates lists inside given JSON blob to a given size. 

166 

167 Args: 

168 blob (dict): Blob to be truncated. 

169 size (int, optional): Size of lists. Default 3. 

170 

171 Raises: 

172 EnforceError: If blob is not a dict. 

173 

174 Returns: 

175 dict: Truncated blob. 

176 ''' 

177 Enforce(blob, 'instance of', dict, message='Blob must be a dict.') 

178 # -------------------------------------------------------------------------- 

179 

180 def recurse_list(items, size): 

181 output = [] 

182 for item in truncate_list(items, size=size): 

183 if isinstance(item, dict): 

184 item = recurse(item) 

185 elif isinstance(item, list): 

186 item = recurse_list(item, size) 

187 output.append(item) 

188 return output 

189 

190 def recurse(item): 

191 output = {} 

192 for k, v in item.items(): 

193 if isinstance(v, dict): 

194 output[k] = recurse(v) 

195 elif isinstance(v, list): 

196 output[k] = recurse_list(v, size) 

197 else: 

198 output[k] = v 

199 return output 

200 return recurse(blob) 

201 

202 

203# LOGGING----------------------------------------------------------------------- 

204def log_runtime( 

205 function, *args, message_=None, _testing=False, log_level='info', **kwargs 

206): 

207 # type (Callable, ..., Optional[str], bool, str, ...) -> Any 

208 r''' 

209 Logs the duration of given function called with given arguments. 

210 

211 Args: 

212 function (function): Function to be called. 

213 \*args (object, optional): Arguments. 

214 message_ (str, optional): Message to be returned. Default: None. 

215 _testing (bool, optional): Returns message if True. Default: False. 

216 log_level (str, optional): Log level. Default: info. 

217 \*\*kwargs (object, optional): Keyword arguments. 

218 

219 Raises: 

220 EnforceError: If log level is illegal. 

221 

222 Returns: 

223 object: function(*args, **kwargs). 

224 ''' 

225 level = log_level_to_int(log_level) 

226 

227 # this may silently break file writes in multiprocessing 

228 stopwatch = StopWatch() 

229 stopwatch.start() 

230 output = function(*args, **kwargs) 

231 stopwatch.stop() 

232 

233 if message_ is not None: 

234 message_ += f'\n Runtime: {stopwatch.human_readable_delta}' 

235 else: 

236 message_ = f'''{function.__name__} 

237 Runtime: {stopwatch.human_readable_delta} 

238 Args: {pformat(args)} 

239 Kwargs: {pformat(kwargs)}''' 

240 

241 if _testing: 

242 return message_ 

243 

244 LOGGER.log(level, message_) 

245 return output 

246 

247 

248@wrapt.decorator 

249def runtime(wrapped, instance, args, kwargs): 

250 # type: (Callable, Any, Any, Any) -> Any 

251 r''' 

252 Decorator for logging the duration of given function called with given 

253 arguments. 

254 

255 Args: 

256 wrapped (function): Function to be called. 

257 instance (object): Needed by wrapt. 

258 \*args (object, optional): Arguments. 

259 \*\*kwargs (object, optional): Keyword arguments. 

260 

261 Returns: 

262 function: Wrapped function. 

263 ''' 

264 return log_runtime(wrapped, *args, **kwargs) 

265 

266 

267def log_level_to_int(level): 

268 # type: (Union[str, int]) -> int 

269 ''' 

270 Convert a given string or integer into a log level integer. 

271 

272 Args: 

273 level (str or int): Log level. 

274 

275 Raises: 

276 EnforceError: If level is illegal. 

277 

278 Returns: 

279 int: Log level as integer. 

280 ''' 

281 keys = ['critical', 'debug', 'error', 'fatal', 'info', 'warn', 'warning'] 

282 values = [getattr(logging, x.upper()) for x in keys] 

283 lut = dict(zip(keys, values)) # type: Dict[str, int] 

284 

285 msg = 'Log level must be an integer or string. Given value: {a}. ' 

286 lut_msg = ', '.join([f'{k}: {v}' for k, v in zip(keys, values)]) 

287 msg += f'Legal values: [{lut_msg}].' 

288 

289 output = 0 

290 if isinstance(level, int): 

291 Enforce(level, 'in', values, message=msg) 

292 output = level 

293 

294 elif isinstance(level, str): 

295 level = level.lower() 

296 Enforce(level, 'in', keys, message=msg) 

297 output = lut[level] 

298 

299 else: 

300 raise EnforceError(msg.format(a=level)) 

301 

302 return output 

303 

304 

305class LogRuntime: 

306 ''' 

307 LogRuntime is a class for logging the runtime of arbitrary code. 

308 

309 Attributes: 

310 message (str): Logging message with runtime line. 

311 delta (datetime.timedelta): Runtime. 

312 human_readable_delta (str): Runtime in human readable format. 

313 

314 Example: 

315 

316 >>> import time 

317 >>> def foobar(): 

318 time.sleep(1) 

319 

320 >>> with LogRuntime('Foo the bars', name=foobar.__name__, level='debug'): 

321 foobar() 

322 DEBUG:foobar:Foo the bars - Runtime: 0:00:01.001069 (1 second) 

323 

324 >>> with LogRuntime(message='Fooing all the bars', suppress=True) as log: 

325 foobar() 

326 >>> print(log.message) 

327 Fooing all the bars - Runtime: 0:00:01.001069 (1 second) 

328 ''' 

329 def __init__( 

330 self, 

331 message='', # type: str 

332 name='LogRuntime', # type: str 

333 level='info', # type: str 

334 suppress=False, # type: bool 

335 message_func=None, # type: Optional[Callable[[str, StopWatch], None]] 

336 callback=None, # type: Optional[Callable[[str], Any]] 

337 ): 

338 # type: (...) -> None 

339 ''' 

340 Constructs a LogRuntime instance. 

341 

342 Args: 

343 message (str, optional): Logging message. Default: ''. 

344 name (str, optional): Name of logger. Default: 'LogRuntime'. 

345 level (str or int, optional): Log level. Default: info. 

346 suppress (bool, optional): Whether to suppress logging. 

347 Default: False. 

348 message_func (function, optional): Custom message function of the 

349 signature (message, StopWatch) -> str. Default: None. 

350 callback (function, optional): Callback function of the signature 

351 (message) -> Any. Default: None. 

352 

353 Raises: 

354 EnforceError: If message is not a string. 

355 EnforceError: If name is not a string. 

356 EnforceError: If level is not legal logging level. 

357 EnforceError: If suppress is not a boolean. 

358 ''' 

359 Enforce(message, 'instance of', str) 

360 Enforce(name, 'instance of', str) 

361 Enforce(suppress, 'instance of', bool) 

362 # ---------------------------------------------------------------------- 

363 

364 self._message = message 

365 self._stopwatch = StopWatch() 

366 self._logger = logging.getLogger(name) 

367 self._level = log_level_to_int(level) 

368 self._suppress = suppress 

369 self._message_func = message_func 

370 self._callback = callback 

371 

372 @staticmethod 

373 def _default_message_func(message, stopwatch): 

374 # type: (str, StopWatch) -> str 

375 ''' 

376 Add runtime information to message given StopWatch instance. 

377 

378 Args: 

379 message (str): Message. 

380 stopwatch (StopWatch): StopWatch instance. 

381 

382 Raises: 

383 EnforeceError: If Message is not a string. 

384 EnforceError: If stopwatch is not a StopWatch instance. 

385 

386 Returns: 

387 str: Message with runtime information. 

388 ''' 

389 Enforce(message, 'instance of', str) 

390 Enforce(stopwatch, 'instance of', StopWatch) 

391 # ---------------------------------------------------------------------- 

392 

393 msg = f'Runtime: {stopwatch.delta} ' 

394 msg += f'({stopwatch.human_readable_delta})' 

395 if message != '': 

396 msg = message + ' - ' + msg 

397 return msg 

398 

399 def __enter__(self): 

400 # type: () -> LogRuntime 

401 ''' 

402 Starts stopwatch. 

403 

404 Returns: 

405 LogRuntime: self. 

406 ''' 

407 self._stopwatch.start() 

408 return self 

409 

410 def __exit__(self, *args): 

411 # type: (Any) -> None 

412 ''' 

413 Stops stopwatch and logs message. 

414 ''' 

415 stopwatch = self._stopwatch 

416 stopwatch.stop() 

417 self.delta = self._stopwatch.delta 

418 self.human_readable_delta = self._stopwatch.human_readable_delta 

419 

420 msg_func = self._message_func or self._default_message_func 

421 self.message = msg_func(self._message, stopwatch) 

422 

423 if not self._suppress: 

424 self._logger.log(self._level, self.message) 

425 

426 if self._callback is not None: 

427 self._callback(str(self.message)) 

428 

429 

430# HTTP-REQUESTS----------------------------------------------------------------- 

431def post_to_slack(url, channel, message): 

432 # type (str, str, str) -> urllib.request.HttpResponse 

433 ''' 

434 Post a given message to a given slack channel. 

435 

436 Args: 

437 url (str): https://hooks.slack.com/services URL. 

438 channel (str): Channel name. 

439 message (str): Message to be posted. 

440 

441 Raises: 

442 EnforceError: If URL is not a string. 

443 EnforceError: If URL does not start with https://hooks.slack.com/services 

444 EnforceError: If channel is not a string. 

445 EnforceError: If message is not a string. 

446 

447 Returns: 

448 HTTPResponse: Response. 

449 ''' 

450 Enforce(url, 'instance of', str) 

451 Enforce(channel, 'instance of', str) 

452 Enforce(message, 'instance of', str) 

453 msg = 'URL must begin with https://hooks.slack.com/services/. ' 

454 msg += f'Given URL: {url}' 

455 Enforce( 

456 url.startswith('https://hooks.slack.com/services/'), '==', True, 

457 message=msg 

458 ) 

459 # -------------------------------------------------------------------------- 

460 

461 request = urllib.request.Request( 

462 url, 

463 method='POST', 

464 headers={'Content-type': 'application/json'}, 

465 data=json.dumps(dict( 

466 channel='#' + channel, 

467 text=message, 

468 )).encode(), 

469 ) 

470 return urllib.request.urlopen(request) 

471 

472 

473# API--------------------------------------------------------------------------- 

474def get_function_signature(function): 

475 # type: (Callable) -> Dict 

476 ''' 

477 Inspect a given function and return its arguments as a list and its keyword 

478 arguments as a dict. 

479 

480 Args: 

481 function (function): Function to be inspected. 

482 

483 Returns: 

484 dict: args and kwargs. 

485 ''' 

486 spec = inspect.getfullargspec(function) 

487 args = list(spec.args) 

488 kwargs = {} # type: Any 

489 if spec.defaults is not None: 

490 args = args[:-len(spec.defaults)] 

491 kwargs = list(spec.args)[-len(spec.defaults):] 

492 kwargs = dict(zip(kwargs, spec.defaults)) 

493 return dict(args=args, kwargs=kwargs) 

494 

495 

496def _dir_table(obj, public=True, semiprivate=True, private=False, max_width=100): 

497 # type: (Any, bool, bool, bool, int) -> str 

498 ''' 

499 Create a table from results of calling dir(obj). 

500 

501 Args: 

502 obj (object): Object to call dir on. 

503 public (bool, optional): Include public attributes in table. 

504 Default: True. 

505 semiprivate (bool, optional): Include semiprivate attributes in table. 

506 Default: True. 

507 private (bool, optional): Include private attributes in table. 

508 Default: False. 

509 max_width (int, optional): Maximum table width: Default: 100. 

510 

511 Returns: 

512 str: Table. 

513 ''' 

514 dirkeys = dir(obj) 

515 pub = set(list(filter(lambda x: re.search('^[^_]', x), dirkeys))) 

516 priv = set(list(filter(lambda x: re.search('^__|^_[A-Z].*__', x), dirkeys))) 

517 semipriv = set(dirkeys).difference(pub).difference(priv) 

518 

519 keys = set() 

520 if public: 

521 keys.update(pub) 

522 if private: 

523 keys.update(priv) 

524 if semiprivate: 

525 keys.update(semipriv) 

526 

527 data = [dict(key='NAME', atype='TYPE', val='VALUE')] 

528 max_key = 0 

529 max_atype = 0 

530 for key in sorted(keys): 

531 attr = getattr(obj, key) 

532 atype = attr.__class__.__name__ 

533 val = str(attr).replace('\n', ' ') 

534 max_key = max(max_key, len(key)) 

535 max_atype = max(max_atype, len(atype)) 

536 row = dict(key=key, atype=atype, val=val) 

537 data.append(row) 

538 

539 pattern = f'{{key:<{max_key}}} {{atype:<{max_atype}}} {{val}}' 

540 lines = list(map(lambda x: pattern.format(**x)[:max_width], data)) 

541 output = '\n'.join(lines) 

542 return output 

543 

544 

545def dir_table(obj, public=True, semiprivate=True, private=False, max_width=100): 

546 # type: (Any, bool, bool, bool, int) -> None 

547 ''' 

548 Prints a table from results of calling dir(obj). 

549 

550 Args: 

551 obj (object): Object to call dir on. 

552 public (bool, optional): Include public attributes in table. 

553 Default: True. 

554 semiprivate (bool, optional): Include semiprivate attributes in table. 

555 Default: True. 

556 private (bool, optional): Include private attributes in table. 

557 Default: False. 

558 max_width (int, optional): Maximum table width: Default: 100. 

559 ''' 

560 print(_dir_table( 

561 obj, 

562 public=public, 

563 semiprivate=semiprivate, 

564 private=private, 

565 max_width=max_width, 

566 )) # pragma: no cover 

567 

568 

569def api_function(wrapped=None, **kwargs): 

570 r''' 

571 A decorator that enforces keyword argument only function signatures and 

572 required keyword argument values when called. 

573 

574 Args: 

575 wrapped (function): For dev use. Default: None. 

576 \*\*kwargs (dict): Keyword arguments. # noqa: W605 

577 

578 Raises: 

579 TypeError: If non-keyword argument found in functionn signature. 

580 ValueError: If keyword arg with value of '<required>' is found. 

581 

582 Returns: 

583 api function. 

584 ''' 

585 @wrapt.decorator 

586 def wrapper(wrapped, instance, args, kwargs): 

587 sig = get_function_signature(wrapped) 

588 

589 # ensure no arguments are present 

590 if len(sig['args']) > 0: 

591 msg = 'Function may only have keyword arguments. ' 

592 msg += f"Found non-keyword arguments: {sig['args']}." 

593 raise TypeError(msg) 

594 

595 # ensure all required kwarg values are present 

596 params = sig['kwargs'] 

597 params.update(kwargs) 

598 for key, val in params.items(): 

599 if val == '<required>': 

600 msg = f'Missing required keyword argument: {key}.' 

601 raise ValueError(msg) 

602 

603 LOGGER.debug(f'{wrapped} called with {params}.') 

604 return wrapped(*args, **kwargs) 

605 return wrapper(wrapped) 

606 

607 

608def format_block(text, dedent=0, indent=0, collapse=True): 

609 # type: (str, int, int, bool) -> str 

610 ''' 

611 Format text block by a given indent. 

612 Also strips leading and trailing newlines. 

613 

614 Args: 

615 text (str): Text to be indented. 

616 dedent (int, optional): Amount of indent to be removed. Default: 0. 

617 indent (int, optional): Amount of indent. Default: 0. 

618 collapse (bool, optional): If text is just whitespace, return a null 

619 string. Default: True. 

620 

621 Returns: 

622 str: Indented text. 

623 ''' 

624 if collapse and re.search(r'^\s*$', text): 

625 return '' 

626 

627 line = re.sub('^ *| *$', '', text) 

628 lines = line.lstrip('\n').rstrip('\n').split('\n') 

629 buff = '^' + ' ' * dedent 

630 lines = [re.sub(buff, '', x) for x in lines] 

631 

632 # indent by given amount 

633 ident = ' ' * indent 

634 lines = [re.sub('^', ident, x) for x in lines] 

635 output = '\n'.join(lines) 

636 return output 

637 

638 

639def autoformat_block(text): 

640 # type: (str) -> str 

641 ''' 

642 Determine block indentation and format text block with 0 indentation. 

643 

644 Args: 

645 text (str): Text. 

646 

647 Returns: 

648 str: Dedented text. 

649 ''' 

650 lines = text.lstrip('\n').rstrip('\n').split('\n') 

651 lines = list(filter(lambda x: not re.search('^ *$', x), lines)) 

652 tmp = [re.search('^ *', x).group(0) for x in lines] # type: ignore 

653 indents = list(map(len, tmp)) 

654 if indents == []: 

655 indents = [0] 

656 indent = min(indents) 

657 return format_block(text, dedent=indent) 

658 

659 

660def traverse_directory( 

661 directory, include_regex='', exclude_regex='', entry_type='file' 

662): 

663 # type: (FilePath, str, str, str) -> Generator[Path, None, None] 

664 ''' 

665 Recusively list all files or directories within a given directory. 

666 

667 Args: 

668 directory (str or Path): Directory to walk. 

669 include_regex (str, optional): Include filenames that match this regex. 

670 Default: ''. 

671 exclude_regex (str, optional): Exclude filenames that match this regex. 

672 Default: ''. 

673 entry_type (str, optional): Kind of directory entry to return. Options 

674 include: file, directory. Default: file. 

675 

676 Raises: 

677 FileNotFoundError: If argument is not a directory or does not exist. 

678 EnforceError: If entry_type is not file or directory. 

679 

680 Yields: 

681 Path: File. 

682 ''' 

683 etypes = ['file', 'directory'] 

684 msg = 'Illegal entry type: {a}. Legal entry types: {b}.' 

685 Enforce(entry_type, 'in', etypes, message=msg) 

686 

687 directory = Path(directory) 

688 if not directory.is_dir(): 

689 msg = f'{directory} is not a directory or does not exist.' 

690 raise FileNotFoundError(msg) 

691 # -------------------------------------------------------------------------- 

692 

693 include_re = re.compile(include_regex) 

694 exclude_re = re.compile(exclude_regex) 

695 

696 for root, dirs, items in os.walk(directory): 

697 if entry_type == 'directory': 

698 items = dirs 

699 for item in items: 

700 filepath = Path(root, item) 

701 

702 output = True 

703 temp = filepath.absolute().as_posix() 

704 if include_regex != '' and not include_re.search(temp): 

705 output = False 

706 if exclude_regex != '' and exclude_re.search(temp): 

707 output = False 

708 

709 if output: 

710 yield filepath 

711 

712 

713def str_to_bool(string): 

714 # type: (str) -> bool 

715 ''' 

716 Converts a string to a boolean value. 

717 

718 Args: 

719 string (str): String to be converted. 

720 

721 Returns: 

722 bool: Boolean 

723 ''' 

724 if string.lower() == 'true': 

725 return True 

726 return False 

727# ------------------------------------------------------------------------------ 

728 

729 

730class RegexMatch: 

731 ''' 

732 A convenience class for using regular expressions in match statements. 

733 ''' 

734 def __init__(self, string): 

735 # type: (str) -> None 

736 ''' 

737 Construct a RegexMatch instance. 

738 

739 Args: 

740 string (str): String to match pattern against. 

741 ''' 

742 self.string = string 

743 

744 def __eq__(self, pattern): 

745 # type: (object) -> bool 

746 ''' 

747 Check if string matches pattern. 

748 

749 Args: 

750 pattern (str): Regular expression pattern. 

751 

752 Returns: 

753 bool: True if string matches pattern. 

754 ''' 

755 return bool(re.search(str(pattern), self.string)) 

756# ------------------------------------------------------------------------------ 

757 

758 

759def is_standard_module(name): 

760 # type: (str) -> bool 

761 ''' 

762 Determines if given module name is a python builtin. 

763 

764 Args: 

765 name (str): Python module name. 

766 

767 Returns: 

768 bool: Whether string names a python module. 

769 ''' 

770 return name in _PYTHON_STANDARD_MODULES 

771 

772 

773_PYTHON_STANDARD_MODULES = [ 

774 '__future__', 

775 '__main__', 

776 '_dummy_thread', 

777 '_thread', 

778 'abc', 

779 'aifc', 

780 'argparse', 

781 'array', 

782 'ast', 

783 'asynchat', 

784 'asyncio', 

785 'asyncore', 

786 'atexit', 

787 'audioop', 

788 'base64', 

789 'bdb', 

790 'binascii', 

791 'binhex', 

792 'bisect', 

793 'builtins', 

794 'bz2', 

795 'calendar', 

796 'cgi', 

797 'cgitb', 

798 'chunk', 

799 'cmath', 

800 'cmd', 

801 'code', 

802 'codecs', 

803 'codeop', 

804 'collections', 

805 'collections.abc', 

806 'colorsys', 

807 'compileall', 

808 'concurrent', 

809 'concurrent.futures', 

810 'configparser', 

811 'contextlib', 

812 'contextvars', 

813 'copy', 

814 'copyreg', 

815 'cProfile', 

816 'crypt', 

817 'csv', 

818 'ctypes', 

819 'curses', 

820 'curses.ascii', 

821 'curses.panel', 

822 'curses.textpad', 

823 'dataclasses', 

824 'datetime', 

825 'dbm', 

826 'dbm.dumb', 

827 'dbm.gnu', 

828 'dbm.ndbm', 

829 'decimal', 

830 'difflib', 

831 'dis', 

832 'distutils', 

833 'distutils.archive_util', 

834 'distutils.bcppcompiler', 

835 'distutils.ccompiler', 

836 'distutils.cmd', 

837 'distutils.command', 

838 'distutils.command.bdist', 

839 'distutils.command.bdist_dumb', 

840 'distutils.command.bdist_msi', 

841 'distutils.command.bdist_packager', 

842 'distutils.command.bdist_rpm', 

843 'distutils.command.bdist_wininst', 

844 'distutils.command.build', 

845 'distutils.command.build_clib', 

846 'distutils.command.build_ext', 

847 'distutils.command.build_py', 

848 'distutils.command.build_scripts', 

849 'distutils.command.check', 

850 'distutils.command.clean', 

851 'distutils.command.config', 

852 'distutils.command.install', 

853 'distutils.command.install_data', 

854 'distutils.command.install_headers', 

855 'distutils.command.install_lib', 

856 'distutils.command.install_scripts', 

857 'distutils.command.register', 

858 'distutils.command.sdist', 

859 'distutils.core', 

860 'distutils.cygwinccompiler', 

861 'distutils.debug', 

862 'distutils.dep_util', 

863 'distutils.dir_util', 

864 'distutils.dist', 

865 'distutils.errors', 

866 'distutils.extension', 

867 'distutils.fancy_getopt', 

868 'distutils.file_util', 

869 'distutils.filelist', 

870 'distutils.log', 

871 'distutils.msvccompiler', 

872 'distutils.spawn', 

873 'distutils.sysconfig', 

874 'distutils.text_file', 

875 'distutils.unixccompiler', 

876 'distutils.util', 

877 'distutils.version', 

878 'doctest', 

879 'dummy_threading', 

880 'email', 

881 'email.charset', 

882 'email.contentmanager', 

883 'email.encoders', 

884 'email.errors', 

885 'email.generator', 

886 'email.header', 

887 'email.headerregistry', 

888 'email.iterators', 

889 'email.message', 

890 'email.mime', 

891 'email.parser', 

892 'email.policy', 

893 'email.utils', 

894 'encodings', 

895 'encodings.idna', 

896 'encodings.mbcs', 

897 'encodings.utf_8_sig', 

898 'ensurepip', 

899 'enum', 

900 'errno', 

901 'faulthandler', 

902 'fcntl', 

903 'filecmp', 

904 'fileinput', 

905 'fnmatch', 

906 'formatter', 

907 'fractions', 

908 'ftplib', 

909 'functools', 

910 'gc', 

911 'getopt', 

912 'getpass', 

913 'gettext', 

914 'glob', 

915 'grp', 

916 'gzip', 

917 'hashlib', 

918 'heapq', 

919 'hmac', 

920 'html', 

921 'html.entities', 

922 'html.parser', 

923 'http', 

924 'http.client', 

925 'http.cookiejar', 

926 'http.cookies', 

927 'http.server', 

928 'imaplib', 

929 'imghdr', 

930 'imp', 

931 'importlib', 

932 'importlib.abc', 

933 'importlib.machinery', 

934 'importlib.resources', 

935 'importlib.util', 

936 'inspect', 

937 'io', 

938 'ipaddress', 

939 'itertools', 

940 'json', 

941 'json.tool', 

942 'keyword', 

943 'lib2to3', 

944 'linecache', 

945 'locale', 

946 'logging', 

947 'logging.config', 

948 'logging.handlers', 

949 'lzma', 

950 'macpath', 

951 'mailbox', 

952 'mailcap', 

953 'marshal', 

954 'math', 

955 'mimetypes', 

956 'mmap', 

957 'modulefinder', 

958 'msilib', 

959 'msvcrt', 

960 'multiprocessing', 

961 'multiprocessing.connection', 

962 'multiprocessing.dummy', 

963 'multiprocessing.managers', 

964 'multiprocessing.pool', 

965 'multiprocessing.sharedctypes', 

966 'netrc', 

967 'nis', 

968 'nntplib', 

969 'numbers', 

970 'operator', 

971 'optparse', 

972 'os', 

973 'os.path', 

974 'ossaudiodev', 

975 'parser', 

976 'pathlib', 

977 'pdb', 

978 'pickle', 

979 'pickletools', 

980 'pipes', 

981 'pkgutil', 

982 'platform', 

983 'plistlib', 

984 'poplib', 

985 'posix', 

986 'pprint', 

987 'profile', 

988 'pstats', 

989 'pty', 

990 'pwd', 

991 'py_compile', 

992 'pyclbr', 

993 'pydoc', 

994 'queue', 

995 'quopri', 

996 'random', 

997 're', 

998 'readline', 

999 'reprlib', 

1000 'resource', 

1001 'rlcompleter', 

1002 'runpy', 

1003 'sched', 

1004 'secrets', 

1005 'select', 

1006 'selectors', 

1007 'shelve', 

1008 'shlex', 

1009 'shutil', 

1010 'signal', 

1011 'site', 

1012 'smtpd', 

1013 'smtplib', 

1014 'sndhdr', 

1015 'socket', 

1016 'socketserver', 

1017 'spwd', 

1018 'sqlite3', 

1019 'ssl', 

1020 'stat', 

1021 'statistics', 

1022 'string', 

1023 'stringprep', 

1024 'struct', 

1025 'subprocess', 

1026 'sunau', 

1027 'symbol', 

1028 'symtable', 

1029 'sys', 

1030 'sysconfig', 

1031 'syslog', 

1032 'tabnanny', 

1033 'tarfile', 

1034 'telnetlib', 

1035 'tempfile', 

1036 'termios', 

1037 'test', 

1038 'test.support', 

1039 'test.support.script_helper', 

1040 'textwrap', 

1041 'threading', 

1042 'time', 

1043 'timeit', 

1044 'tkinter', 

1045 'tkinter.scrolledtext', 

1046 'tkinter.tix', 

1047 'tkinter.ttk', 

1048 'token', 

1049 'tokenize', 

1050 'trace', 

1051 'traceback', 

1052 'tracemalloc', 

1053 'tty', 

1054 'turtle', 

1055 'turtledemo', 

1056 'types', 

1057 'typing', 

1058 'unicodedata', 

1059 'unittest', 

1060 'unittest.mock', 

1061 'urllib', 

1062 'urllib.error', 

1063 'urllib.parse', 

1064 'urllib.request', 

1065 'urllib.response', 

1066 'urllib.robotparser', 

1067 'uu', 

1068 'uuid', 

1069 'venv', 

1070 'warnings', 

1071 'wave', 

1072 'weakref', 

1073 'webbrowser', 

1074 'winreg', 

1075 'winsound', 

1076 'wsgiref', 

1077 'wsgiref.handlers', 

1078 'wsgiref.headers', 

1079 'wsgiref.simple_server', 

1080 'wsgiref.util', 

1081 'wsgiref.validate', 

1082 'xdrlib', 

1083 'xml', 

1084 'xml.dom', 

1085 'xml.dom.minidom', 

1086 'xml.dom.pulldom', 

1087 'xml.etree.ElementTree', 

1088 'xml.parsers.expat', 

1089 'xml.parsers.expat.errors', 

1090 'xml.parsers.expat.model', 

1091 'xml.sax', 

1092 'xml.sax.handler', 

1093 'xml.sax.saxutils', 

1094 'xml.sax.xmlreader', 

1095 'xmlrpc', 

1096 'xmlrpc.client', 

1097 'xmlrpc.server', 

1098 'zipapp', 

1099 'zipfile', 

1100 'zipimport', 

1101 'zlib' 

1102] # type: List[str]