A VCS repository archival tool
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

206 řádky
6.4 KiB

  1. import argparse
  2. import contextlib
  3. import datetime
  4. import inspect
  5. import logging
  6. import os
  7. import requests.models
  8. # Imported in parse_args() and main() after setting up the logger:
  9. #import codearchiver.core
  10. #import codearchiver.modules
  11. #import codearchiver.storage
  12. #import codearchiver.version
  13. import tempfile
  14. ## Logging
  15. dumpLocals = False
  16. _logger = logging # Replaced below after setting the logger class
  17. class Logger(logging.Logger):
  18. def _log_with_stack(self, level, *args, **kwargs):
  19. super().log(level, *args, **kwargs)
  20. if dumpLocals:
  21. stack = inspect.stack()
  22. if len(stack) >= 3:
  23. name = _dump_stack_and_locals(stack[2:][::-1])
  24. super().log(level, f'Dumped stack and locals to {name}')
  25. def warning(self, *args, **kwargs):
  26. self._log_with_stack(logging.WARNING, *args, **kwargs)
  27. def error(self, *args, **kwargs):
  28. self._log_with_stack(logging.ERROR, *args, **kwargs)
  29. def critical(self, *args, **kwargs):
  30. self._log_with_stack(logging.CRITICAL, *args, **kwargs)
  31. def log(self, level, *args, **kwargs):
  32. if level >= logging.WARNING:
  33. self._log_with_stack(level, *args, **kwargs)
  34. else:
  35. super().log(level, *args, **kwargs)
  36. def _requests_preparedrequest_repr(name, request):
  37. ret = []
  38. ret.append(repr(request))
  39. ret.append(f'\n {name}.method = {request.method}')
  40. ret.append(f'\n {name}.url = {request.url}')
  41. ret.append(f'\n {name}.headers = \\')
  42. for field in request.headers:
  43. ret.append(f'\n {field} = {_repr("_", request.headers[field])}')
  44. if request.body:
  45. ret.append(f'\n {name}.body = ')
  46. ret.append(_repr('_', request.body).replace('\n', '\n '))
  47. return ''.join(ret)
  48. def _requests_response_repr(name, response, withHistory = True):
  49. ret = []
  50. ret.append(repr(response))
  51. ret.append(f'\n {name}.url = {response.url}')
  52. ret.append(f'\n {name}.request = ')
  53. ret.append(_repr('_', response.request).replace('\n', '\n '))
  54. if withHistory and response.history:
  55. ret.append(f'\n {name}.history = [')
  56. for previousResponse in response.history:
  57. ret.append(f'\n ')
  58. ret.append(_requests_response_repr('_', previousResponse, withHistory = False).replace('\n', '\n '))
  59. ret.append('\n ]')
  60. ret.append(f'\n {name}.status_code = {response.status_code}')
  61. ret.append(f'\n {name}.headers = \\')
  62. for field in response.headers:
  63. ret.append(f'\n {field} = {_repr("_", response.headers[field])}')
  64. ret.append(f'\n {name}.content = {_repr("_", response.content)}')
  65. return ''.join(ret)
  66. def _repr(name, value):
  67. if type(value) is requests.models.Response:
  68. return _requests_response_repr(name, value)
  69. if type(value) is requests.models.PreparedRequest:
  70. return _requests_preparedrequest_repr(name, value)
  71. valueRepr = repr(value)
  72. if '\n' in valueRepr:
  73. return ''.join(['\\\n ', valueRepr.replace('\n', '\n ')])
  74. return valueRepr
  75. @contextlib.contextmanager
  76. def _dump_locals_on_exception():
  77. try:
  78. yield
  79. except Exception as e:
  80. trace = inspect.trace()
  81. if len(trace) >= 2:
  82. name = _dump_stack_and_locals(trace[1:], exc = e)
  83. _logger.fatal(f'Dumped stack and locals to {name}')
  84. raise
  85. def _dump_stack_and_locals(trace, exc = None):
  86. with tempfile.NamedTemporaryFile('w', prefix = 'codearchiver_locals_', delete = False) as fp:
  87. if exc is not None:
  88. fp.write('Exception:\n')
  89. fp.write(f' {type(exc).__module__}.{type(exc).__name__}: {exc!s}\n')
  90. fp.write(f' args: {exc.args!r}\n')
  91. fp.write('\n')
  92. fp.write('Stack:\n')
  93. for frameRecord in trace:
  94. fp.write(f' File "{frameRecord.filename}", line {frameRecord.lineno}, in {frameRecord.function}\n')
  95. for line in frameRecord.code_context:
  96. fp.write(f' {line.strip()}\n')
  97. fp.write('\n')
  98. for frameRecord in trace:
  99. module = inspect.getmodule(frameRecord[0])
  100. if not module.__name__.startswith('codearchiver.') and module.__name__ != 'codearchiver':
  101. continue
  102. locals_ = frameRecord[0].f_locals
  103. fp.write(f'Locals from file "{frameRecord.filename}", line {frameRecord.lineno}, in {frameRecord.function}:\n')
  104. for variableName in locals_:
  105. variable = locals_[variableName]
  106. varRepr = _repr(variableName, variable)
  107. fp.write(f' {variableName} {type(variable)} = ')
  108. fp.write(varRepr.replace('\n', '\n '))
  109. fp.write('\n')
  110. fp.write('\n')
  111. if 'self' in locals_ and hasattr(locals_['self'], '__dict__'):
  112. fp.write(f'Object dict:\n')
  113. fp.write(repr(locals_['self'].__dict__))
  114. fp.write('\n\n')
  115. name = fp.name
  116. return name
  117. def parse_args():
  118. import codearchiver.version
  119. parser = argparse.ArgumentParser(formatter_class = argparse.ArgumentDefaultsHelpFormatter)
  120. parser.add_argument('--version', action = 'version', version = f'codearchiver {codearchiver.version.__version__}')
  121. parser.add_argument('-v', '--verbose', '--verbosity', dest = 'verbosity', action = 'count', default = 0, help = 'Increase output verbosity')
  122. parser.add_argument('--dump-locals', dest = 'dumpLocals', action = 'store_true', default = False, help = 'Dump local variables on serious log messages (warnings or higher)')
  123. parser.add_argument('url', help = 'Target URL')
  124. args = parser.parse_args()
  125. return args
  126. def setup_logging():
  127. logging.setLoggerClass(Logger)
  128. global _logger
  129. _logger = logging.getLogger(__name__)
  130. def configure_logging(verbosity, dumpLocals_):
  131. global dumpLocals
  132. dumpLocals = dumpLocals_
  133. rootLogger = logging.getLogger()
  134. # Set level
  135. if verbosity > 0:
  136. level = logging.INFO if verbosity == 1 else logging.DEBUG
  137. rootLogger.setLevel(level)
  138. for handler in rootLogger.handlers:
  139. handler.setLevel(level)
  140. # Create formatter
  141. formatter = logging.Formatter('{asctime}.{msecs:03.0f} {levelname} {name} {message}', datefmt = '%Y-%m-%d %H:%M:%S', style = '{')
  142. # Remove existing handlers
  143. for handler in rootLogger.handlers:
  144. rootLogger.removeHandler(handler)
  145. # Add stream handler
  146. handler = logging.StreamHandler()
  147. handler.setFormatter(formatter)
  148. rootLogger.addHandler(handler)
  149. def main():
  150. setup_logging()
  151. args = parse_args()
  152. configure_logging(args.verbosity, args.dumpLocals)
  153. import codearchiver.core
  154. import codearchiver.modules
  155. import codearchiver.storage
  156. with _dump_locals_on_exception():
  157. inputUrl = codearchiver.core.InputURL(args.url)
  158. storage = codearchiver.storage.DirectoryStorage(os.getcwd())
  159. module = codearchiver.core.get_module_instance(inputUrl, storage = storage)
  160. with tempfile.TemporaryDirectory(prefix = 'tmp.codearchiver.', dir = os.getcwd()) as td:
  161. _logger.debug(f'Running in {td}')
  162. os.chdir(td)
  163. try:
  164. result = module.process()
  165. storage.put_result(result)
  166. finally:
  167. os.chdir('..')
  168. if __name__ == '__main__':
  169. main()