naeural-client 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. naeural_client/__init__.py +13 -0
  2. naeural_client/_ver.py +13 -0
  3. naeural_client/base/__init__.py +6 -0
  4. naeural_client/base/distributed_custom_code_presets.py +44 -0
  5. naeural_client/base/generic_session.py +1763 -0
  6. naeural_client/base/instance.py +616 -0
  7. naeural_client/base/payload/__init__.py +1 -0
  8. naeural_client/base/payload/payload.py +66 -0
  9. naeural_client/base/pipeline.py +1499 -0
  10. naeural_client/base/plugin_template.py +5209 -0
  11. naeural_client/base/responses.py +209 -0
  12. naeural_client/base/transaction.py +157 -0
  13. naeural_client/base_decentra_object.py +143 -0
  14. naeural_client/bc/__init__.py +3 -0
  15. naeural_client/bc/base.py +1046 -0
  16. naeural_client/bc/chain.py +0 -0
  17. naeural_client/bc/ec.py +324 -0
  18. naeural_client/certs/__init__.py +0 -0
  19. naeural_client/certs/r9092118.ala.eu-central-1.emqxsl.com.crt +22 -0
  20. naeural_client/code_cheker/__init__.py +1 -0
  21. naeural_client/code_cheker/base.py +520 -0
  22. naeural_client/code_cheker/checker.py +294 -0
  23. naeural_client/comm/__init__.py +2 -0
  24. naeural_client/comm/amqp_wrapper.py +338 -0
  25. naeural_client/comm/mqtt_wrapper.py +539 -0
  26. naeural_client/const/README.md +3 -0
  27. naeural_client/const/__init__.py +9 -0
  28. naeural_client/const/base.py +101 -0
  29. naeural_client/const/comms.py +80 -0
  30. naeural_client/const/environment.py +26 -0
  31. naeural_client/const/formatter.py +7 -0
  32. naeural_client/const/heartbeat.py +111 -0
  33. naeural_client/const/misc.py +20 -0
  34. naeural_client/const/payload.py +190 -0
  35. naeural_client/default/__init__.py +1 -0
  36. naeural_client/default/instance/__init__.py +4 -0
  37. naeural_client/default/instance/chain_dist_custom_job_01_plugin.py +54 -0
  38. naeural_client/default/instance/custom_web_app_01_plugin.py +118 -0
  39. naeural_client/default/instance/net_mon_01_plugin.py +45 -0
  40. naeural_client/default/instance/view_scene_01_plugin.py +28 -0
  41. naeural_client/default/session/mqtt_session.py +72 -0
  42. naeural_client/io_formatter/__init__.py +2 -0
  43. naeural_client/io_formatter/base/__init__.py +1 -0
  44. naeural_client/io_formatter/base/base_formatter.py +80 -0
  45. naeural_client/io_formatter/default/__init__.py +3 -0
  46. naeural_client/io_formatter/default/a_dummy.py +51 -0
  47. naeural_client/io_formatter/default/aixp1.py +113 -0
  48. naeural_client/io_formatter/default/default.py +22 -0
  49. naeural_client/io_formatter/io_formatter_manager.py +96 -0
  50. naeural_client/logging/__init__.py +1 -0
  51. naeural_client/logging/base_logger.py +2056 -0
  52. naeural_client/logging/logger_mixins/__init__.py +12 -0
  53. naeural_client/logging/logger_mixins/class_instance_mixin.py +92 -0
  54. naeural_client/logging/logger_mixins/computer_vision_mixin.py +443 -0
  55. naeural_client/logging/logger_mixins/datetime_mixin.py +344 -0
  56. naeural_client/logging/logger_mixins/download_mixin.py +421 -0
  57. naeural_client/logging/logger_mixins/general_serialization_mixin.py +242 -0
  58. naeural_client/logging/logger_mixins/json_serialization_mixin.py +481 -0
  59. naeural_client/logging/logger_mixins/pickle_serialization_mixin.py +301 -0
  60. naeural_client/logging/logger_mixins/process_mixin.py +63 -0
  61. naeural_client/logging/logger_mixins/resource_size_mixin.py +81 -0
  62. naeural_client/logging/logger_mixins/timers_mixin.py +501 -0
  63. naeural_client/logging/logger_mixins/upload_mixin.py +260 -0
  64. naeural_client/logging/logger_mixins/utils_mixin.py +675 -0
  65. naeural_client/logging/small_logger.py +93 -0
  66. naeural_client/logging/tzlocal/__init__.py +20 -0
  67. naeural_client/logging/tzlocal/unix.py +231 -0
  68. naeural_client/logging/tzlocal/utils.py +113 -0
  69. naeural_client/logging/tzlocal/win32.py +151 -0
  70. naeural_client/logging/tzlocal/windows_tz.py +718 -0
  71. naeural_client/plugins_manager_mixin.py +273 -0
  72. naeural_client/utils/__init__.py +2 -0
  73. naeural_client/utils/comm_utils.py +44 -0
  74. naeural_client/utils/dotenv.py +75 -0
  75. naeural_client-2.0.0.dist-info/METADATA +365 -0
  76. naeural_client-2.0.0.dist-info/RECORD +78 -0
  77. naeural_client-2.0.0.dist-info/WHEEL +4 -0
  78. naeural_client-2.0.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,520 @@
1
+ import io
2
+ import zlib
3
+ import sys
4
+ import base64
5
+ import traceback
6
+ import re
7
+ import inspect
8
+ import ctypes
9
+ import threading
10
+ import queue
11
+
12
+ from .checker import ASTChecker, CheckerConstants
13
+
14
+ __VER__ = '0.6.1'
15
+
16
+ UNALLOWED_DICT = {
17
+ 'import ': {
18
+ 'error': 'Imports are not allowed in plugin code ',
19
+ 'type': 'import',
20
+ },
21
+
22
+ 'from ': {
23
+ 'error': 'Imports are not allowed in plugin code ',
24
+ 'type': 'import',
25
+ },
26
+
27
+ 'globals': {
28
+ 'error': 'Global vars access is not allowed in plugin code ',
29
+ 'type': CheckerConstants.var,
30
+ },
31
+
32
+ '__builtins__': {
33
+ 'error': '__builtins__ access is not allowed in plugin code ',
34
+ 'type': CheckerConstants.var,
35
+ },
36
+
37
+ 'locals': {
38
+ 'error': 'Local vars dict access is not allowed in plugin code ',
39
+ 'type': CheckerConstants.var,
40
+ },
41
+
42
+ 'memoryview': {
43
+ 'error': 'Pointer handling is unsafe in plugin code ',
44
+ 'type': CheckerConstants.var,
45
+ },
46
+
47
+ 'log': {
48
+ 'error': 'Logger object cannot be used directly in plugin code - please use API ',
49
+ 'type': CheckerConstants.attr,
50
+ },
51
+
52
+ 'vars': {
53
+ 'error': 'Usage of `vars(obj)` is not allowed in plugin code ',
54
+ 'type': CheckerConstants.var,
55
+ },
56
+
57
+ 'dir': {
58
+ 'error': 'Usage of `dir(obj)` is not allowed in plugin code ',
59
+ 'type': CheckerConstants.var,
60
+ },
61
+
62
+ 'global_shmem': {
63
+ 'error': 'Usage of `global_shmem` is not allowed in plugin code ',
64
+ 'type': CheckerConstants.attr,
65
+ },
66
+
67
+ 'plugins_shmem': {
68
+ 'error': 'Usage of `plugins_shmem` is not allowed in plugin code ',
69
+ 'type': CheckerConstants.attr,
70
+ },
71
+
72
+ 'config_data': {
73
+ 'error': 'Usage of `config_data` is not allowed in plugin code ',
74
+ 'type': CheckerConstants.attr,
75
+ },
76
+
77
+ '_default_config': {
78
+ 'error': 'Usage of `_default_config` is not allowed in plugin code ',
79
+ 'type': CheckerConstants.attr,
80
+ },
81
+
82
+ '__traceback__': {
83
+ 'error': 'Usage of `__traceback__` as an attribute is not allowed in plugin code ',
84
+ 'type': CheckerConstants.attr,
85
+ },
86
+
87
+ '_upstream_config': {
88
+ 'error': 'Usage of `_upstream_config` is not allowed in plugin code ',
89
+ 'type': CheckerConstants.attr,
90
+ },
91
+
92
+ 'exec': {
93
+ 'error': 'Usage of `exec()` is not allowed in plugin code ',
94
+ 'type': CheckerConstants.var,
95
+ },
96
+
97
+ 'eval': {
98
+ 'error': 'Usage of `eval()` is not allowed in plugin code ',
99
+ 'type': CheckerConstants.var,
100
+ },
101
+
102
+ 'getattr': {
103
+ 'error': 'Usage of `getattr()` is not allowed in plugin code ',
104
+ 'type': CheckerConstants.var,
105
+ },
106
+
107
+ 'open': {
108
+ 'error': 'Usage of `open()` is not allowed in plugin code ',
109
+ 'type': CheckerConstants.var,
110
+ }
111
+ }
112
+
113
+ RESULT_VARS = ['__result', '_result', 'result']
114
+
115
+
116
+ class CodeExecutionTimeoutError(Exception):
117
+ pass
118
+
119
+
120
+ class BaseCodeChecker:
121
+ """
122
+ This class should be used either as a associated object for code checking or
123
+ as a mixin for running code
124
+ """
125
+
126
+ def __init__(self):
127
+ super(BaseCodeChecker, self).__init__()
128
+ self.printed_lines = []
129
+ self.__exec_code_lock = threading.Lock()
130
+ return
131
+
132
+ def __msg(self, m, color='d'):
133
+ if hasattr(self, 'P'):
134
+ self.P(m, color=color)
135
+ elif hasattr(self, 'log'):
136
+ self.log.P(m, color=color)
137
+ else:
138
+ print(m)
139
+ return
140
+
141
+ def _is_safe_import(self, code, safe_imports):
142
+ if safe_imports is None:
143
+ return False
144
+ for imp in safe_imports:
145
+ if imp in code:
146
+ return True
147
+ return False
148
+
149
+ def _check_unsafe_code(self, code, safe_imports=None):
150
+ checker = ASTChecker(UNALLOWED_DICT, safe_imports)
151
+ errors = checker.validate(code)
152
+ if len(errors) == 0:
153
+ return None
154
+ return errors
155
+
156
+ # PUB
157
+
158
+ def check_code_text(self, code, safe_imports=None):
159
+ return self._check_unsafe_code(code, safe_imports=safe_imports)
160
+
161
+ def str_to_base64(self, str, verbose=False, compress=False):
162
+ l_i = len(str)
163
+ l_c = -1
164
+ b_str = bytes(str, 'utf-8')
165
+ if compress:
166
+ b_str = zlib.compress(b_str, level=9)
167
+ l_c = sys.getsizeof(b_str)
168
+ b_encoded = base64.b64encode(b_str)
169
+ str_encoded = b_encoded.decode('utf-8')
170
+ l_b64 = len(str_encoded)
171
+ if verbose:
172
+ self.__msg("Initial/Compress/B64: {}/{}/{}".format(
173
+ l_i, l_c, l_b64), color='g'
174
+ )
175
+ return str_encoded
176
+
177
+ def code_to_base64(self, code, verbose=False, compress=True, return_errors=False):
178
+ if verbose:
179
+ self.__msg("Processing:\n{}".format(code), color='y')
180
+ errors = self._check_unsafe_code(code)
181
+ if errors is not None:
182
+ err_msg = "Cannot serialize code due to: '{}'".format(errors)
183
+ self.__msg(err_msg, color='r')
184
+ return None if not return_errors else (None, err_msg)
185
+ self.__msg("Code checking succeeded", color='g')
186
+ str_encoded = self.str_to_base64(code, verbose=verbose, compress=compress)
187
+ return str_encoded if not return_errors else (str_encoded, None)
188
+
189
+ def base64_to_code(self, b64code, decompress=True):
190
+ decoded = None
191
+ try:
192
+ b_decoded = base64.b64decode(b64code)
193
+ if decompress:
194
+ b_decoded = zlib.decompress(b_decoded)
195
+ s_decoded = b_decoded.decode('utf-8')
196
+ decoded = s_decoded
197
+ except:
198
+ pass
199
+ return decoded
200
+
201
+ def prepare_b64code(self, str_b64code, check_for_result=True, result_vars=RESULT_VARS):
202
+ errors, code = None, None
203
+ code = self.base64_to_code(str_b64code)
204
+ to_check_code = code
205
+ if code is not None:
206
+ if self._can_encapsulate_code_in_method(code):
207
+ # we have a return statement in the code,
208
+ # so we need to encapsulate the code in a function
209
+ to_check_code = self._encapsulate_code_in_method(
210
+ exec_code__code=code,
211
+ exec_code__arguments=[]
212
+ )
213
+ # endif can encapsulate code in method
214
+ errors = self._check_unsafe_code(to_check_code)
215
+ if errors is None:
216
+ if code is None:
217
+ errors = 'Provided ascii data is not a valid base64 object'
218
+ # endif no valid code provided
219
+ # endif no errors
220
+ return code, errors
221
+
222
+ def _add_line_after_each_line(self, code, codeline='plugin.sleep(0.001)'):
223
+ lines = code.splitlines()
224
+ refactor = []
225
+ has_loop = False
226
+ for line in lines:
227
+ rstripped = line.rstrip()
228
+ stripped = line.lstrip()
229
+ is_loop = stripped.startswith(('while', 'for'))
230
+ has_loop = has_loop or is_loop
231
+ if is_loop and rstripped[-1] != ':':
232
+ parts = line.split(':')
233
+ if len(parts) > 0:
234
+ line = parts[0] + ': ' + codeline + ';' + parts[1]
235
+ has_loop = False # loop solved
236
+ elif has_loop and not is_loop:
237
+ nspc = len(line) - len(stripped)
238
+ spc = nspc * ' '
239
+ refactor.append(spc + codeline)
240
+ has_loop = False # loop solved
241
+ # endif
242
+ refactor.append(line)
243
+ str_refactor = '\n'.join(refactor)
244
+ return str_refactor
245
+
246
+ def _can_encapsulate_code_in_method(self, exec_code__code):
247
+ return re.search(r'\breturn\b', exec_code__code) is not None
248
+
249
+ def _encapsulate_code_in_method(self, exec_code__code, exec_code__arguments):
250
+ for i in range(len(exec_code__arguments)):
251
+ __var = exec_code__arguments[i]
252
+ if isinstance(__var, tuple):
253
+ exec_code__arguments[i] = f"{__var[0]}={__var[1]}"
254
+ exec_code__arguments = ', '.join(exec_code__arguments)
255
+
256
+ if re.search(r'\breturn\b', exec_code__code) is not None:
257
+ # we have a return statement in the code,
258
+ # so we need to encapsulate the code in a function
259
+ # 1. indent the code
260
+ exec_code__code = "\n".join([' ' + l for l in exec_code__code.splitlines()])
261
+ # 2. add the function definition
262
+ exec_code__code = "{}\n{}".format(
263
+ f"def __exec_code__({exec_code__arguments}):",
264
+ exec_code__code,
265
+ )
266
+ return exec_code__code
267
+
268
+ def custom_print(self, print_queue, *args, **kwargs):
269
+ """
270
+ Custom print function that will be used in the plugin code.
271
+ """
272
+ # redirect the print to the plugin cache
273
+ outstream = io.StringIO()
274
+ print(*args, file=outstream, **kwargs)
275
+ printed_value = outstream.getvalue()
276
+ print_queue.put(printed_value)
277
+ # print to the console
278
+ print(f'[CUST_CODE_PRINT]{printed_value}')
279
+ return
280
+
281
+ def execute_code(self, code, local_vars, output_queue, print_queue):
282
+ exec_code__result_var = None
283
+ exec_code__warnings = []
284
+
285
+ local_vars['print'] = lambda *args, **kwargs: self.custom_print(print_queue, *args, **kwargs)
286
+ try:
287
+ # Execute the code
288
+ with self.__exec_code_lock:
289
+ exec(code, local_vars)
290
+
291
+ # Capture result variables
292
+ for _var in local_vars.get('exec_code__result_vars', []):
293
+ if _var in local_vars:
294
+ exec_code__result_var = local_vars[_var]
295
+ break
296
+ # endfor all result vars
297
+ # endwith lock
298
+ # Put the results in the queue
299
+ output_queue.put({
300
+ "result_var": exec_code__result_var,
301
+ "warnings": exec_code__warnings,
302
+ "error": None
303
+ })
304
+ except Exception as e:
305
+ output_queue.put({
306
+ "result_var": None,
307
+ "warnings": exec_code__warnings,
308
+ "error": traceback.format_exc()
309
+ })
310
+
311
+ def __code_exec_stop_thread(self, thread):
312
+ """
313
+ Stop the specified thread.
314
+ Parameters
315
+ ----------
316
+ thread : threading.Thread
317
+ The thread to stop.
318
+ """
319
+ tid = thread.ident
320
+ res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(SystemExit))
321
+ if res == 0:
322
+ raise ValueError("Invalid thread ID")
323
+ elif res > 1:
324
+ # If it modifies more than one thread, something went wrong, so revert it
325
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0)
326
+ raise SystemError("PyThreadState_SetAsyncExc failed")
327
+
328
+ def execute_code_with_timeout(self, code, timeout, local_vars=None):
329
+ if local_vars is None:
330
+ local_vars = {}
331
+
332
+ # Queue to collect output
333
+ output_queue = queue.Queue()
334
+ print_queue = queue.Queue()
335
+
336
+ # Create a separate thread for code execution
337
+ thread = threading.Thread(target=self.execute_code, args=(code, local_vars, output_queue, print_queue))
338
+ thread.daemon = True
339
+ # process = multiprocessing.Process(target=self.execute_code, args=(code, local_vars, output_queue, print_queue))
340
+
341
+ # Start the process
342
+ # process.start()
343
+ thread.start()
344
+
345
+ # Wait for the process to complete or timeout
346
+ # process.join(timeout)
347
+ thread.join(timeout)
348
+
349
+ # If process is still alive after timeout, terminate it
350
+ if thread.is_alive():
351
+ # process.terminate()
352
+ # thread.join()
353
+ # TODO: maybe still send partial results or prints?
354
+ self.__code_exec_stop_thread(thread)
355
+
356
+ return {
357
+ "result_var": None,
358
+ "warnings": [],
359
+ "printed_lines": [],
360
+ "error": f"Code execution took longer than {timeout} seconds."
361
+ }
362
+ # endif process is still alive
363
+
364
+ printed_lines = []
365
+ while not print_queue.empty():
366
+ printed_lines.append(print_queue.get())
367
+ # Get the output from the queue
368
+ if not output_queue.empty():
369
+ exec_result = output_queue.get()
370
+ exec_result['printed_lines'] = printed_lines
371
+ return exec_result
372
+
373
+ return {
374
+ "result_var": None,
375
+ "warnings": [],
376
+ "printed_lines": printed_lines,
377
+ "error": "No result returned."
378
+ }
379
+
380
+ def exec_code(self, str_b64code, debug=False, result_vars=None, self_var=None, modify=True, return_printed=False, timeout=None):
381
+ exec_code__result_vars = result_vars or RESULT_VARS
382
+ exec_code__warnings = []
383
+ exec_code__result_var = None
384
+
385
+ # Prepare the code
386
+ exec_code__code, exec_code__errors = self.prepare_b64code(str_b64code, result_vars=exec_code__result_vars)
387
+
388
+ if exec_code__errors:
389
+ self.__msg(f"Cannot execute remote code: {exec_code__errors}", color='r')
390
+ return exec_code__result_var, exec_code__errors, exec_code__warnings
391
+
392
+ # Optionally modify the code
393
+ if modify:
394
+ exec_code__code = self._add_line_after_each_line(code=exec_code__code)
395
+
396
+ if debug:
397
+ self.__msg(f"DEBUG EXEC: Executing:\n{exec_code__code}")
398
+
399
+ # Add `self` to locals if specified
400
+ local_vars = locals().copy()
401
+ if self_var and isinstance(self_var, str) and len(self_var) > 3:
402
+ local_vars[self_var] = self
403
+
404
+ # Handle encapsulating the code in a method if needed
405
+ if self._can_encapsulate_code_in_method(exec_code__code):
406
+ exec_code__code = self._encapsulate_code_in_method(exec_code__code, exec_code__arguments=[self_var])
407
+ exec_code__code = f"{exec_code__code}\nresult = __exec_code__({self_var})"
408
+
409
+ # Prepare to capture printed output
410
+ self.printed_lines = []
411
+ local_vars['print'] = self.custom_print
412
+
413
+ # Execute the code with a timeout
414
+ exec_result = self.execute_code_with_timeout(exec_code__code, timeout, local_vars=local_vars)
415
+
416
+ exec_code__result_var = exec_result.get("result_var")
417
+ exec_code__errors = exec_result.get("error")
418
+ exec_code__warnings.extend(exec_result.get("warnings"))
419
+
420
+ # Collect results
421
+ res = (exec_code__result_var, exec_code__errors, exec_code__warnings)
422
+
423
+ if return_printed:
424
+ res += (exec_result.get("printed_lines", []),)
425
+
426
+ return res
427
+
428
+ def _get_method_from_custom_code(self, str_b64code, debug=False, result_vars=RESULT_VARS, self_var=None, modify=True, method_arguments=[]):
429
+ exec_code__result_vars = result_vars
430
+ exec_code__debug = debug
431
+ exec_code__self_var = self_var
432
+ exec_code__modify = modify
433
+ exec_code__warnings = []
434
+ exec_code__code, exec_code__errors = self.prepare_b64code(
435
+ str_b64code,
436
+ result_vars=exec_code__result_vars,
437
+ )
438
+ exec_code__result_var = None
439
+ has_result = False
440
+ if exec_code__errors is not None:
441
+ self.__msg("Cannot execute remote code: {}".format(exec_code__errors), color='r')
442
+ return exec_code__result_var, exec_code__errors, exec_code__warnings
443
+
444
+ # code does not have any safety errors
445
+ if exec_code__modify:
446
+ exec_code__code = self._add_line_after_each_line(code=exec_code__code)
447
+ if exec_code__debug:
448
+ self.__msg("DEBUG EXEC: Executing: \n{}".format(exec_code__code))
449
+ if exec_code__self_var is not None and isinstance(exec_code__self_var, str) and len(exec_code__self_var) > 3:
450
+ locals()[exec_code__self_var] = self
451
+
452
+ try:
453
+ if self._can_encapsulate_code_in_method(exec_code__code):
454
+ # we have a return statement in the code,
455
+ # so we need to encapsulate the code in a function
456
+ exec_code__code = self._encapsulate_code_in_method(
457
+ exec_code__code=exec_code__code,
458
+ exec_code__arguments=method_arguments
459
+ )
460
+ exec_code__code = "{}\n{}".format(
461
+ exec_code__code,
462
+ f"result = __exec_code__"
463
+ )
464
+ else:
465
+ # in this case we want to have our code in a method
466
+ # so we will break here
467
+ exec_code__errors = ["Cannot encapsulate code in method. No return statement found."]
468
+ return exec_code__result_var, exec_code__errors, exec_code__warnings
469
+ # endif can encapsulate code in method
470
+
471
+ exec(exec_code__code)
472
+ if exec_code__debug:
473
+ self.__msg("DEBUG EXEC: locals(): \n{}".format(locals()))
474
+ for _var in exec_code__result_vars:
475
+ if _var in locals():
476
+ if exec_code__debug:
477
+ self.__msg("DEBUG EXEC: Extracting var '{}' from {}".format(_var, locals()))
478
+ exec_code__result_var = locals().get(_var)
479
+ has_result = True
480
+ break
481
+ if not has_result:
482
+ exec_code__warnings.append("No result variable is set. Possible options: {}".format(exec_code__result_vars))
483
+ except Exception as e:
484
+ exec_code__result_var = None
485
+ if hasattr(self, 'log'):
486
+ exec_code__errors = list(self.log.get_error_info())
487
+ exec_code__errors.append(traceback.format_exc())
488
+ else:
489
+ exec_code__errors = str(e)
490
+ # end try-except
491
+ return exec_code__result_var, exec_code__errors, exec_code__warnings
492
+
493
+ def method_to_base64(self, func, verbose=False):
494
+ code = self.get_function_source_code(func)
495
+ return self.code_to_base64(code, verbose=verbose)
496
+
497
+ def get_function_source_code(self, func):
498
+ """
499
+ Get the source code of a function and remove the indentation.
500
+
501
+ Parameters
502
+ ----------
503
+ func : Callable
504
+ The function.
505
+
506
+ Returns
507
+ -------
508
+ str
509
+ The source code of the function.
510
+ """
511
+ plain_code = inspect.getsourcelines(func)[0]
512
+ plain_code = plain_code[1:]
513
+ first_code_line = 0
514
+ # ignore empty lines at the beginning, but keep them
515
+ while plain_code[first_code_line].strip() == '':
516
+ first_code_line += 1
517
+ indent = len(plain_code[first_code_line]) - len(plain_code[first_code_line].lstrip())
518
+ plain_code = '\n'.join([line.rstrip()[indent:] for line in plain_code])
519
+
520
+ return plain_code