oocana-python-executor 0.15.3__tar.gz → 0.16.0__tar.gz

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 (18) hide show
  1. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/PKG-INFO +3 -1
  2. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/pyproject.toml +6 -1
  3. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/block.py +4 -4
  4. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/context.py +2 -2
  5. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/executor.py +40 -18
  6. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/service.py +1 -1
  7. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/tests/test_cli.py +1 -1
  8. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/__init__.py +0 -0
  9. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/data.py +0 -0
  10. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/hook.py +0 -0
  11. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/logger.py +0 -0
  12. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/matplot/matplotlib_oomol/__init__.py +0 -0
  13. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/matplot/matplotlib_oomol/oomol.py +0 -0
  14. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/matplot/oomol_matplot_helper.py +0 -0
  15. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/secret.py +0 -0
  16. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/topic.py +0 -0
  17. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/python_executor/utils.py +0 -0
  18. {oocana_python_executor-0.15.3 → oocana_python_executor-0.16.0}/tests/test_secret.py +0 -0
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: oocana-python-executor
3
- Version: 0.15.3
3
+ Version: 0.16.0
4
4
  Summary: a client subscribe mqtt topic to execute oocana's block
5
5
  Author-Email: l1shen <lishen1635@gmail.com>, yleaf <11785335+leavesster@users.noreply.github.com>
6
6
  License: MIT
7
7
  Requires-Python: >=3.9
8
8
  Requires-Dist: oocana
9
+ Provides-Extra: debug
10
+ Requires-Dist: debugpy; extra == "debug"
9
11
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "oocana-python-executor"
3
- version = "0.15.3"
3
+ version = "0.16.0"
4
4
  authors = [
5
5
  { name = "l1shen", email = "lishen1635@gmail.com" },
6
6
  { name = "yleaf", email = "11785335+leavesster@users.noreply.github.com" },
@@ -14,6 +14,11 @@ dependencies = [
14
14
  [project.license]
15
15
  text = "MIT"
16
16
 
17
+ [project.optional-dependencies]
18
+ debug = [
19
+ "debugpy",
20
+ ]
21
+
17
22
  [project.scripts]
18
23
  python-executor = "python_executor.executor:main"
19
24
 
@@ -70,24 +70,24 @@ def load_module(file_path: str, source_dir=None):
70
70
 
71
71
  def output_return_object(obj, context: Context):
72
72
  if obj is None:
73
- context.done()
73
+ context.done() if not context.is_done else None
74
74
  elif obj is context.keepAlive:
75
75
  pass
76
76
  elif isinstance(obj, dict):
77
77
  for k, v in obj.items():
78
78
  context.output(k, v)
79
- context.done()
79
+ context.done() if not context.is_done else None
80
80
  else:
81
81
  context.done(f"return object needs to be a dictionary, but get type: {type(obj)}")
82
82
 
83
83
  logger = logging.getLogger(EXECUTOR_NAME)
84
84
 
85
- async def run_block(message, mainframe: Mainframe, session_dir: str):
85
+ async def run_block(message, mainframe: Mainframe, session_dir: str, tmp_dir: str):
86
86
 
87
87
  logger.info(f"block {message.get('job_id')} start")
88
88
  try:
89
89
  payload = ExecutePayload(**message)
90
- context = createContext(mainframe, payload.session_id, payload.job_id, store, payload.outputs, session_dir)
90
+ context = createContext(mainframe, payload.session_id, payload.job_id, store, payload.outputs, session_dir, tmp_dir = tmp_dir)
91
91
  except Exception:
92
92
  traceback_str = traceback.format_exc()
93
93
  # rust 那边会保证传过来的 message 一定是符合格式的,所以这里不应该出现异常。这里主要是防止 rust 修改错误。
@@ -9,7 +9,7 @@ from .data import EXECUTOR_NAME
9
9
  logger = logging.getLogger(EXECUTOR_NAME)
10
10
 
11
11
  def createContext(
12
- mainframe: Mainframe, session_id: str, job_id: str, store, output, session_dir: str
12
+ mainframe: Mainframe, session_id: str, job_id: str, store, output, session_dir: str, tmp_dir: str
13
13
  ) -> Context:
14
14
 
15
15
  node_props = mainframe.notify_block_ready(session_id, job_id)
@@ -59,7 +59,7 @@ def createContext(
59
59
 
60
60
  blockInfo = BlockInfo(**node_props)
61
61
 
62
- ctx = Context(inputs, blockInfo, mainframe, store, output, session_dir)
62
+ ctx = Context(inputs, blockInfo, mainframe, store, output, session_dir, tmp_dir)
63
63
  # 跟 executor 日志分开,避免有的库在 logger 里面使用 print,导致 hook 出现递归调用。
64
64
  block_logger = logging.getLogger(f"block {job_id}")
65
65
  ctx_handler = ContextHandler(ctx)
@@ -19,9 +19,9 @@ service_store: dict[str, Literal["launching", "running"]] = {}
19
19
  job_set = set()
20
20
 
21
21
  # 日志目录 ~/.oocana/sessions/{session_id}
22
- # executor 的日志都会记录在 [python-executor-{suffix}.log | python-executor.log]
23
- # 全局 logger 会记录在 python-{suffix}.log | python.log
24
- def config_logger(session_id: str, suffix: str | None, output: Literal["console", "file"]):
22
+ # executor 的日志都会记录在 [python-executor-{identifier}.log | python-executor.log]
23
+ # 全局 logger 会记录在 python-{identifier}.log | python.log
24
+ def config_logger(session_id: str, identifier: str | None, output: Literal["console", "file"]):
25
25
 
26
26
 
27
27
  format = '%(asctime)s - %(levelname)s - {%(pathname)s:%(lineno)d} - %(message)s'
@@ -29,7 +29,7 @@ def config_logger(session_id: str, suffix: str | None, output: Literal["console"
29
29
  logger.setLevel(logging.DEBUG)
30
30
  if output == "file":
31
31
  executor_dir = os.path.join(oocana_dir(), "sessions", session_id)
32
- logger_file = os.path.join(executor_dir, f"python-executor-{suffix}.log") if suffix is not None else os.path.join(executor_dir, "python-executor.log")
32
+ logger_file = os.path.join(executor_dir, f"python-executor-{identifier}.log") if identifier is not None else os.path.join(executor_dir, "python-executor.log")
33
33
 
34
34
  if not os.path.exists(logger_file):
35
35
  os.makedirs(os.path.dirname(logger_file), exist_ok=True)
@@ -37,7 +37,7 @@ def config_logger(session_id: str, suffix: str | None, output: Literal["console"
37
37
  print(f"setup logging in file {logger_file}")
38
38
  h = logging.FileHandler(logger_file)
39
39
 
40
- global_logger_file = os.path.join(executor_dir, f"python-{suffix}.log") if suffix is not None else os.path.join(executor_dir, "python.log")
40
+ global_logger_file = os.path.join(executor_dir, f"python-{identifier}.log") if identifier is not None else os.path.join(executor_dir, "python.log")
41
41
  logging.basicConfig(filename=global_logger_file, level=logging.DEBUG, format=format)
42
42
  else:
43
43
  logging.basicConfig(level=logging.DEBUG, format=format)
@@ -49,10 +49,10 @@ def config_logger(session_id: str, suffix: str | None, output: Literal["console"
49
49
  logger.propagate = False
50
50
 
51
51
 
52
- async def run_executor(address: str, session_id: str, package: str | None, session_dir: str, suffix: str | None = None, identifier: str | None = None):
52
+ async def run_executor(address: str, session_id: str, tmp_dir: str, package: str | None, session_dir: str, identifier: str | None = None):
53
53
 
54
- if suffix is not None:
55
- mainframe = Mainframe(address, f"python-executor-{suffix}", logger)
54
+ if identifier is not None:
55
+ mainframe = Mainframe(address, f"python-executor-id-{identifier}", logger)
56
56
  else:
57
57
  mainframe = Mainframe(address, f"python-executor-{session_id}", logger)
58
58
 
@@ -211,12 +211,12 @@ async def run_executor(address: str, session_id: str, package: str | None, sessi
211
211
  else:
212
212
  if not_current_session(message):
213
213
  continue
214
- run_block_in_new_thread(message, mainframe, session_dir=session_dir)
214
+ run_block_in_new_thread(message, mainframe, session_dir=session_dir, tmp_dir=tmp_dir)
215
215
 
216
- def run_block_in_new_thread(message, mainframe: Mainframe, session_dir: str):
216
+ def run_block_in_new_thread(message, mainframe: Mainframe, session_dir: str, tmp_dir: str):
217
217
 
218
218
  async def run():
219
- await run_block(message, mainframe, session_dir=session_dir)
219
+ await run_block(message, mainframe, session_dir=session_dir, tmp_dir=tmp_dir)
220
220
  run_in_new_thread(run)
221
221
 
222
222
  def main():
@@ -224,26 +224,48 @@ def main():
224
224
  import argparse
225
225
  parser = argparse.ArgumentParser(description="run executor with address, session-id, tmp-dir")
226
226
  parser.add_argument("--session-id", help="executor subscribe session id", required=True)
227
- parser.add_argument("--address", help="mqtt address", default="mqtt://127.0.0.1:47688")
228
227
  parser.add_argument("--session-dir", help="a tmp dir for whole session", required=True)
228
+ parser.add_argument("--tmp-dir", help="a tmp dir for whole session. It will be cleaned after session success, this behavior is guaranteed by oocana.", required=True)
229
+ parser.add_argument("--address", help="mqtt address", default="mqtt://127.0.0.1:47688")
229
230
  parser.add_argument("--output", help="output log to console or file", default="file", choices=["console", "file"])
230
231
  parser.add_argument("--package", help="package path, if set, executor will only run same package block", default=None)
231
232
  parser.add_argument("--identifier", help="identifier for executor, oocana will think same identifier as one executor", default=None)
232
- parser.add_argument("--suffix", help="suffix for log file", default=None)
233
+ parser.add_argument("--debug-port", help="debug port for python", default=None)
234
+ parser.add_argument("--wait-for-client", help="wait for client to connect", default=False, action="store_true")
233
235
 
234
- args = parser.parse_args()
236
+ try:
237
+ args = parser.parse_args()
238
+ except Exception as e:
239
+ print(f"parse args error: {e}")
240
+ # because we hook sys.exit in hook.py and raise a exception, the exit will be reset to 1.
241
+ # parser origin exit code is 2. so we use 2 here.
242
+ sys.exit(2)
235
243
 
236
244
  address: str = args.address
237
245
  session_id: str = str(args.session_id)
238
246
  output: Literal["console", "file"] = args.output
239
247
  package: str | None = args.package
240
- suffix: str | None = args.suffix
241
248
  session_dir: str = args.session_dir
249
+ tmp_dir: str = args.tmp_dir
242
250
  identifier: str | None = args.identifier
243
251
 
244
- config_logger(session_id, suffix, output)
245
-
246
- run_async_code(run_executor(address=address, session_id=session_id, package=package, session_dir=session_dir, suffix=suffix, identifier=identifier))
252
+ config_logger(session_id, identifier, output)
253
+
254
+ if args.debug_port is not None and args.debug_port.isdigit():
255
+ try:
256
+ import debugpy
257
+ debugpy.listen(int(args.debug_port))
258
+ logger.info(f"debugpy listen on port {args.debug_port}")
259
+ if args.wait_for_client:
260
+ logger.info("wait for client to connect")
261
+ debugpy.wait_for_client()
262
+ logger.info("client connected")
263
+ except ImportError:
264
+ logger.warning("Warning: debugpy not installed, debugging functionality will not be available")
265
+ except Exception as e:
266
+ logger.warning(f"Warning: debugpy listen failed: {e}")
267
+
268
+ run_async_code(run_executor(address=address, tmp_dir=tmp_dir, session_id=session_id, package=package, session_dir=session_dir, identifier=identifier))
247
269
 
248
270
  if __name__ == '__main__':
249
271
  main()
@@ -179,7 +179,7 @@ class ServiceRuntime(ServiceContextAbstractClass):
179
179
  self._runningBlocks.add(job_id)
180
180
  self._jobs.add(job_id)
181
181
 
182
- context = createContext(self._mainframe, payload["session_id"], payload["job_id"], self._store, payload["outputs"], self._session_dir)
182
+ context = createContext(self._mainframe, payload["session_id"], payload["job_id"], self._store, payload["outputs"], self._session_dir, tmp_dir=self._session_dir) # TODO: tmp_dir need consider global service.
183
183
 
184
184
  if isinstance(self.block_handler, dict):
185
185
  handler = self.block_handler.get(block_name)
@@ -20,7 +20,7 @@ class TestExecutorCLI(unittest.TestCase):
20
20
  self.assertIsNone(code, "HTTP server failed to start or exit.")
21
21
 
22
22
  def test_cli(self):
23
- cli_command = [sys.executable, "-u", "-m", "python_executor.executor", "--session-id", "test-session", "--session-dir", "/tmp"]
23
+ cli_command = [sys.executable, "-u", "-m", "python_executor.executor", "--session-id", "test-session", "--session-dir", "/tmp", "--tmp-dir", "/tmp"]
24
24
 
25
25
  print("Starting CLI tool... in", executor_parent_dir)
26
26