pyouter 0.1.2__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.
pyouter/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ from .router import Router
2
+ from .oh_my_zsh.install import install as omz
3
+ from .fish.install import install as fish
4
+ from .bash.install import install as bash
5
+ from .app import App
6
+
7
+
8
+ def runner():
9
+ app = App()
10
+ app.use(
11
+ router=Router(
12
+ install=Router(
13
+ ohmyzsh=omz,
14
+ fish=fish,
15
+ bash=bash)))
16
+ app.run()
pyouter/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env python
2
+ from pyouter import runner
3
+
4
+ if __name__ == "__main__":
5
+ runner()
pyouter/app.py ADDED
@@ -0,0 +1,97 @@
1
+
2
+ from typing import Any
3
+ import asyncio
4
+
5
+ from pyouter.default import create_parser
6
+ from pyouter.errors import NotInit
7
+ from pyouter.router import Router
8
+
9
+ class App(object):
10
+ def __init__(self, config=None, parser=None):
11
+ opt_parser = parser if parser is not None else create_parser("tasks router runner")
12
+
13
+ self.opt_parser = opt_parser
14
+ self.options = None
15
+ self.executor = None
16
+
17
+ if config is None:
18
+ self.config = {}
19
+ elif type(config)==type(''):
20
+ import json
21
+ with open(config,'r') as f:
22
+ self.config = json.load(f)
23
+ elif type(config)==type({}):
24
+ self.config = config
25
+ else:
26
+ raise NotInit("config type unknonw")
27
+
28
+ self.router: Router
29
+
30
+ def use(self, router: Router):
31
+ self.router = router
32
+ return self
33
+
34
+ def option(self, *args: Any, **kwds: Any):
35
+ self.opt_parser.add_argument(*args, **kwds)
36
+ return self
37
+
38
+ def web(self, host="127.0.0.1", port=8765, open_browser=True, focus=None):
39
+ """Start the local web ui for this app's router."""
40
+ if self.router is None:
41
+ raise NotInit("self.router in App")
42
+
43
+ if self.config is None:
44
+ raise NotInit("self.config in App")
45
+
46
+ if self.options is None:
47
+ self.options = self.opt_parser.parse_args()
48
+
49
+ self.router.context(self.config, self.options, self.executor)
50
+
51
+ from pyouter.web import serve
52
+ serve(
53
+ self,
54
+ host=host,
55
+ port=port,
56
+ open_browser=open_browser,
57
+ focus="" if focus is None else focus,
58
+ )
59
+ return self
60
+
61
+ def run(self):
62
+ if self.router is None:
63
+ raise NotInit("self.router in App")
64
+
65
+ if self.config is None:
66
+ raise NotInit("self.config in App")
67
+
68
+ if self.options is None:
69
+ self.options = self.opt_parser.parse_args()
70
+
71
+ self.router.context(self.config, self.options, self.executor)
72
+
73
+ if getattr(self.options, 'web', False):
74
+ self.web(
75
+ host=getattr(self.options, "host", "127.0.0.1"),
76
+ port=getattr(self.options, "port", 8765),
77
+ open_browser=not getattr(self.options, "no_browser", False),
78
+ focus=self.options.actions,
79
+ )
80
+ return self
81
+
82
+ if getattr(self.options, 'tree', False):
83
+ if self.options.actions:
84
+ # 如果有actions参数,显示路径追踪和子树
85
+ self.router.print_tree(self.options.actions)
86
+ else:
87
+ # 如果没有actions参数,显示完整树
88
+ self.router.print_tree()
89
+ return self
90
+
91
+ if self.options.tasks:
92
+ for task in self.router.tasks():
93
+ print(task)
94
+ else:
95
+ asyncio.get_event_loop().run_until_complete(self.router.dispatch('', self.options.actions, 0))
96
+
97
+ return self
File without changes
@@ -0,0 +1,27 @@
1
+ import string
2
+
3
+ def install(config, options):
4
+ command = string.Template("""#!/usr/bin/env pyouter.bash
5
+ # please save this script into your pyouter.bash completion dir and restart
6
+ # your base or source it use
7
+ # . script_name
8
+ # in MacOS with home brew, it is /usr/local/etc/bash_completion.d/
9
+
10
+ _pipeline_router()
11
+ {
12
+ COMPREPLY=($(compgen -W "$$(pyouter --tasks)" "$${COMP_WORDS[1]}"))
13
+ }
14
+
15
+ complete -F _pipeline_router pyouter
16
+
17
+ # if you has used pyouter any old edition, maybe just use below source
18
+ _completion_router()
19
+ {
20
+ COMPREPLY=($(compgen -W "$$(python $script --tasks)" "$${COMP_WORDS[1]}"))
21
+ }
22
+
23
+ complete -F _completion_router $script
24
+ """)
25
+ script_name = options.script if options.script else "__main__.py"
26
+ content = command.substitute(script=script_name)
27
+ print(content)
pyouter/default.py ADDED
@@ -0,0 +1,73 @@
1
+ import argparse
2
+
3
+
4
+ def create_parser(description, tasks="tasks"):
5
+ opt_parser = argparse.ArgumentParser(description=description)
6
+
7
+ opt_parser.add_argument(
8
+ 'actions',
9
+ metavar='A',
10
+ type=str,
11
+ nargs='?',
12
+ help='actions strings in tree')
13
+
14
+ opt_parser.add_argument(
15
+ f'--{tasks}',
16
+ const=True,
17
+ dest='tasks',
18
+ nargs='?',
19
+ help='completion the actions strings'
20
+ )
21
+
22
+ opt_parser.add_argument(
23
+ "-s", '--script-name',
24
+ dest="script",
25
+ nargs='?',
26
+ type=str,
27
+ help='completion the actions strings'
28
+ )
29
+
30
+ opt_parser.add_argument(
31
+ "-v", "--view",
32
+ dest="view",
33
+ help="view the router track",
34
+ action="store_true",
35
+ )
36
+
37
+ opt_parser.add_argument(
38
+ "-t", "--tree",
39
+ dest="tree",
40
+ help="print all available router paths or inspect router tree structure",
41
+ action="store_true",
42
+ )
43
+
44
+ opt_parser.add_argument(
45
+ "-w", "--web",
46
+ dest="web",
47
+ help="start a local web ui to browse and run routers",
48
+ action="store_true",
49
+ )
50
+
51
+ opt_parser.add_argument(
52
+ "--host",
53
+ dest="host",
54
+ default="127.0.0.1",
55
+ help="host for the local web ui (default: 127.0.0.1)",
56
+ )
57
+
58
+ opt_parser.add_argument(
59
+ "--port",
60
+ dest="port",
61
+ default=8765,
62
+ type=int,
63
+ help="port for the local web ui (default: 8765)",
64
+ )
65
+
66
+ opt_parser.add_argument(
67
+ "--no-browser",
68
+ dest="no_browser",
69
+ help="do not open a browser when starting the web ui",
70
+ action="store_true",
71
+ )
72
+
73
+ return opt_parser
pyouter/errors.py ADDED
@@ -0,0 +1,10 @@
1
+ class NotFound(Exception):
2
+ def __init__(self, route, token):
3
+ self.route = route
4
+ self.token = token
5
+ self.message = f"token [{token}] not found in {route}"
6
+
7
+
8
+ class NotInit(Exception):
9
+ def __init__(self, info):
10
+ self.message = f"instance {info} not init"
pyouter/executor.py ADDED
@@ -0,0 +1,46 @@
1
+ import asyncio
2
+ import contextvars
3
+ import inspect
4
+
5
+ from concurrent.futures import Executor
6
+ from functools import partial
7
+
8
+
9
+ class LeafExecutor:
10
+ def __init__(self, config, options, executor, leaf):
11
+ self.config = config
12
+ self.options = options
13
+ self.executor = executor
14
+ self.leaf = leaf
15
+
16
+ async def run(self):
17
+
18
+ def is_continufunction(leaf):
19
+ if inspect.isfunction(leaf):
20
+ return inspect.iscoroutinefunction(leaf)
21
+ else:
22
+ return inspect.iscoroutinefunction(leaf.run)
23
+
24
+ async def run_async(leaf, config, options):
25
+ if inspect.isfunction(leaf):
26
+ return await leaf(config, options)
27
+ else:
28
+ return await leaf.run(config, options)
29
+
30
+ def run_sync(leaf, config, options):
31
+ if inspect.isfunction(leaf):
32
+ return leaf(config, options)
33
+ else:
34
+ return leaf.run(config, options)
35
+
36
+ if is_continufunction(self.leaf):
37
+ await run_async(self.leaf, self.config, self.options)
38
+ else:
39
+ loop = asyncio.get_running_loop()
40
+ # Keep the caller's contextvars in the worker thread so a web run
41
+ # can attribute prints from synchronous actions to the right node.
42
+ ctx = contextvars.copy_context()
43
+ await loop.run_in_executor(
44
+ self.executor,
45
+ partial(ctx.run, partial(run_sync, self.leaf, self.config, self.options)),
46
+ )
File without changes
@@ -0,0 +1,38 @@
1
+ import string
2
+ import os
3
+ import os.path
4
+
5
+
6
+ def install(config, options):
7
+ base = """!/usr/bin/env fish
8
+ complete -c pyouter -f
9
+ complete -c pyouter -a "(python pyouter --tasks)"
10
+ """
11
+ command = string.Template("""
12
+ complete -c $script -f
13
+ complete -c $script -a "(python $script --tasks)"
14
+ """)
15
+ script_name = options.script if options.script else "main.py"
16
+ home = os.getenv("HOME")
17
+ plugin_dir = os.path.join(home, ".config/fish/completions")
18
+ plugin_path = os.path.join(plugin_dir, script_name + ".fish")
19
+
20
+ if not os.path.exists(plugin_dir):
21
+ os.mkdir(plugin_dir)
22
+
23
+ content = ""
24
+ if os.path.exists(plugin_path):
25
+ with open(plugin_path) as f:
26
+ content = f.read()
27
+
28
+ if not content.startswith(base):
29
+ content = base + content
30
+
31
+ plugin = command.substitute(script=script_name)
32
+ if plugin not in content:
33
+ content += plugin
34
+
35
+ with open(plugin_path, "w+") as f:
36
+ f.write(content)
37
+
38
+ print("success")
File without changes
@@ -0,0 +1,72 @@
1
+ import string
2
+ import os
3
+ import os.path
4
+ import re
5
+
6
+
7
+ def install(config, options):
8
+ base = """#compdef
9
+
10
+ _pyouter() {
11
+ compls=$(pyouter --tasks)
12
+
13
+ completions=(${=compls})
14
+ compadd -- $completions
15
+ }
16
+
17
+ compdef _pyouter pyouter
18
+
19
+ """
20
+ command = string.Template("""
21
+ _pipeline() {
22
+ compls=$$(python $script --tasks)
23
+
24
+ completions=($${=compls})
25
+ compadd -- $$completions
26
+ }
27
+
28
+ compdef _pipeline $script
29
+
30
+ """)
31
+ script_name = options.script if options.script else "main.py"
32
+ home = os.getenv("HOME")
33
+ plugin_name = "pyouter"
34
+ plugin_dir = os.path.join(home, ".oh-my-zsh/plugins", plugin_name)
35
+ plugin_path = os.path.join(plugin_dir, plugin_name + ".plugin.zsh")
36
+ zshrc = os.path.join(home, ".zshrc")
37
+ regex = re.compile(r"\nplugins=\(.*?\)")
38
+
39
+ if not os.path.exists(plugin_dir):
40
+ os.mkdir(plugin_dir)
41
+
42
+ with open(zshrc, "r") as f:
43
+ zshrc_content = f.read()
44
+
45
+ m = regex.search(zshrc_content)
46
+ if m is None:
47
+ zshrc_content = zshrc_content + f"\nplugins=({plugin_name})\n"
48
+ else:
49
+ plugins_setting = m.group(0)
50
+ if plugin_name not in plugins_setting:
51
+ plugins_setting = plugins_setting[:-1] + f" {plugin_name}" + plugins_setting[-1]
52
+ prev, post = regex.split(zshrc_content)
53
+ zshrc_content = prev + plugins_setting + post
54
+
55
+ if os.path.exists(plugin_path):
56
+ with open(plugin_path) as f:
57
+ plugin_content = f.read()
58
+ else:
59
+ plugin_content = ""
60
+
61
+ if base not in plugin_content:
62
+ plugin_content = base + plugin_content
63
+ script = command.substitute(script=script_name)
64
+ if script not in plugin_content:
65
+ plugin_content += script
66
+
67
+ with open(plugin_path, "w+") as f:
68
+ f.write(plugin_content)
69
+
70
+ with open(zshrc, "w+") as f:
71
+ f.write(zshrc_content)
72
+ print("success")
pyouter/router.py ADDED
@@ -0,0 +1,139 @@
1
+ import asyncio
2
+
3
+ from .errors import NotFound
4
+ from .executor import LeafExecutor
5
+
6
+ class Router(object):
7
+ def __init__(self, **args):
8
+ self.route = {}
9
+ for key in args:
10
+ self.route[key] = args[key]
11
+
12
+ def context(self, config, options, executor):
13
+ self.config = config
14
+ self.options = options
15
+ self.executor = executor
16
+ for key in self.route:
17
+ router = self.route[key]
18
+ if type(router) == type(self):
19
+ router.context(config, options, executor)
20
+
21
+ async def dispatch(self, pre:str|None, command: str, depth: int):
22
+ show_path = self.options.view or getattr(self.options, 'tree', False)
23
+ path_prefix = ' '*depth if getattr(self.options, 'tree', False) else ''
24
+ run_action = not getattr(self.options, 'tree', False)
25
+
26
+ def full_path(pre, command):
27
+ if pre is not None and pre!='':
28
+ return f'{pre}.{command}'
29
+ else:
30
+ return command
31
+
32
+ if "." in command:
33
+ crt, nxt = command.split('.', 1)
34
+ if crt not in self.route:
35
+ raise NotFound(self.route, crt)
36
+
37
+ router_path = full_path(pre, crt)
38
+ if show_path:
39
+ print(f'[pyouter] {path_prefix}->router: {router_path}')
40
+
41
+ return await self.route[crt].dispatch(router_path, nxt, depth+1)
42
+ else:
43
+ if command not in self.route.keys():
44
+ raise NotFound(self.route, command)
45
+
46
+ leaf = self.route[command]
47
+ if isinstance(leaf, Router):
48
+ # 只在第一次遇到Router时输出路径信息
49
+ router_path = full_path(pre, command)
50
+ if show_path:
51
+ print(f'[pyouter] {path_prefix}->router2: {router_path}')
52
+
53
+ tasks = []
54
+ for crt in leaf.route:
55
+ task = asyncio.create_task(leaf.dispatch(router_path, crt, depth+1))
56
+ tasks.append(task)
57
+
58
+ await asyncio.gather(*tasks, return_exceptions=True)
59
+ else:
60
+ action_path = full_path(pre, command)
61
+ if show_path:
62
+ print(f'[pyouter] {path_prefix}->action: {action_path}')
63
+
64
+ if run_action:
65
+ executor = LeafExecutor(self.config, self.options, self.executor, leaf)
66
+ await executor.run()
67
+
68
+ def tasks(self, base=None):
69
+
70
+ for key in self.route:
71
+ current = f"{base}.{key}" if base else key
72
+ item = self.route[key]
73
+ if type(item) is Router:
74
+ for task in item.tasks(current):
75
+ yield task
76
+ else:
77
+ yield current
78
+
79
+ def print_tree(self, path=None, prefix="", is_last=True):
80
+ """
81
+ 打印树状结构,类似tree命令的输出格式
82
+ path: 要追踪的路径,如 "a.b.c"
83
+ prefix: 当前行的前缀(用于缩进)
84
+ is_last: 是否是当前层级的最后一个节点
85
+ """
86
+ if path:
87
+ # 解析路径,先追踪到目标节点
88
+ parts = path.split('.')
89
+ current_router = self
90
+ current_path = ""
91
+
92
+ # 追踪路径
93
+ for i, part in enumerate(parts):
94
+ if part not in current_router.route:
95
+ print(f"路径 '{path}' 不存在")
96
+ return
97
+
98
+ current_path = f"{current_path}.{part}" if current_path else part
99
+ current_router = current_router.route[part]
100
+
101
+ # 如果不是Router,说明到达叶子节点
102
+ if not isinstance(current_router, Router):
103
+ if i < len(parts) - 1:
104
+ print(f"路径 '{path}' 在 '{current_path}' 处终止(不是Router节点)")
105
+ return
106
+ break
107
+
108
+ # 打印路径追踪
109
+ print(f"路径追踪: {path}")
110
+ print("└── " + path)
111
+
112
+ # 打印目标节点下的完整树
113
+ if isinstance(current_router, Router):
114
+ print(f"\n{current_path} 下的完整树:")
115
+ current_router._print_subtree("", True)
116
+ else:
117
+ print(f"叶子节点: {current_path}")
118
+ else:
119
+ # 打印完整树
120
+ self._print_subtree("", True)
121
+
122
+ def _print_subtree(self, prefix="", is_last=True):
123
+ """
124
+ 打印子树结构
125
+ """
126
+ items = list(self.route.items())
127
+ for i, (key, value) in enumerate(items):
128
+ is_last_item = i == len(items) - 1
129
+ connector = "└── " if is_last_item else "├── "
130
+
131
+ if isinstance(value, Router):
132
+ print(f"{prefix}{connector}{key}/")
133
+ # 递归打印子树
134
+ next_prefix = prefix + (" " if is_last_item else "│ ")
135
+ value._print_subtree(next_prefix, is_last_item)
136
+ else:
137
+ # 叶子节点
138
+ func_name = value.__name__ if hasattr(value, '__name__') else type(value).__name__
139
+ print(f"{prefix}{connector}{key} ({func_name})")