multivisor 6.0.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.
Files changed (34) hide show
  1. multivisor/__init__.py +0 -0
  2. multivisor/client/__init__.py +0 -0
  3. multivisor/client/cli.py +29 -0
  4. multivisor/client/http.py +93 -0
  5. multivisor/client/repl.py +244 -0
  6. multivisor/client/util.py +58 -0
  7. multivisor/multivisor.py +470 -0
  8. multivisor/rpc.py +232 -0
  9. multivisor/server/__init__.py +0 -0
  10. multivisor/server/dist/index.html +1 -0
  11. multivisor/server/dist/static/css/app.2aff3580a128440bba89b94112292cb1.css +6 -0
  12. multivisor/server/dist/static/css/app.2aff3580a128440bba89b94112292cb1.css.map +1 -0
  13. multivisor/server/dist/static/js/app.52791f915c2f060b9cb1.js +2 -0
  14. multivisor/server/dist/static/js/app.52791f915c2f060b9cb1.js.map +1 -0
  15. multivisor/server/dist/static/js/manifest.2ae2e69a05c33dfc65f8.js +2 -0
  16. multivisor/server/dist/static/js/manifest.2ae2e69a05c33dfc65f8.js.map +1 -0
  17. multivisor/server/dist/static/js/vendor.1d02877727062a41e9fb.js +1319 -0
  18. multivisor/server/dist/static/js/vendor.1d02877727062a41e9fb.js.map +1 -0
  19. multivisor/server/rpc.py +156 -0
  20. multivisor/server/tests/__init__.py +0 -0
  21. multivisor/server/tests/conftest.py +1 -0
  22. multivisor/server/tests/test_web.py +194 -0
  23. multivisor/server/util.py +70 -0
  24. multivisor/server/web.py +327 -0
  25. multivisor/signals.py +5 -0
  26. multivisor/tests/__init__.py +0 -0
  27. multivisor/tests/test_multivisor.py +179 -0
  28. multivisor/util.py +75 -0
  29. multivisor-6.0.2.dist-info/METADATA +375 -0
  30. multivisor-6.0.2.dist-info/RECORD +34 -0
  31. multivisor-6.0.2.dist-info/WHEEL +5 -0
  32. multivisor-6.0.2.dist-info/entry_points.txt +4 -0
  33. multivisor-6.0.2.dist-info/licenses/LICENSE +674 -0
  34. multivisor-6.0.2.dist-info/top_level.txt +1 -0
multivisor/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,29 @@
1
+ import argparse
2
+
3
+ import gevent.monkey
4
+
5
+ gevent.monkey.patch_all(thread=False)
6
+
7
+ from .. import util
8
+ from . import repl, http
9
+
10
+
11
+ def parse_args(args=None):
12
+ parser = argparse.ArgumentParser()
13
+ parser.add_argument(
14
+ "--url", help="[http://]<host>[:<22000>]", default="localhost:22000"
15
+ )
16
+ return parser.parse_args(args)
17
+
18
+
19
+ def main(args=None):
20
+ options = parse_args(args)
21
+ url = util.sanitize_url(options.url, protocol="http", port=22000)["url"]
22
+ multivisor = http.Multivisor(url)
23
+ cli = repl.Repl(multivisor)
24
+ gevent.spawn(multivisor.run)
25
+ cli.run()
26
+
27
+
28
+ if __name__ == "__main__":
29
+ main()
@@ -0,0 +1,93 @@
1
+ import json
2
+
3
+ import requests
4
+ from blinker import signal
5
+
6
+
7
+ class Multivisor(object):
8
+ def __init__(self, url):
9
+ self.url = url
10
+ self._status = None
11
+ self.notifications = []
12
+
13
+ def stop_processes(self, *names):
14
+ return self.post("/api/process/stop", data=dict(uid=[",".join(names)]))
15
+
16
+ def restart_processes(self, *names):
17
+ return self.post("/api/process/restart", data=dict(uid=[",".join(names)]))
18
+
19
+ @property
20
+ def status(self):
21
+ if self._status is None:
22
+ self._status = self.get_status()
23
+ return self._status
24
+
25
+ @staticmethod
26
+ def _update_status_stats(status):
27
+ supervisors, processes = status["supervisors"], status["processes"]
28
+ s_stats = dict(
29
+ running=sum((s["running"] for s in status["supervisors"].values())),
30
+ total=len(supervisors),
31
+ )
32
+ s_stats["stopped"] = s_stats["total"] - s_stats["running"]
33
+ p_stats = dict(
34
+ running=sum((p["running"] for p in status["processes"].values())),
35
+ total=len(processes),
36
+ )
37
+ p_stats["stopped"] = p_stats["total"] - p_stats["running"]
38
+ stats = dict(supervisors=s_stats, processes=p_stats)
39
+ status["stats"] = stats
40
+ return stats
41
+
42
+ def get_status(self):
43
+ status = self.get("/api/data").json()
44
+ # reorganize status per process
45
+ status["processes"] = processes = {}
46
+ for supervisor in status["supervisors"].values():
47
+ processes.update(supervisor["processes"])
48
+ self._update_status_stats(status)
49
+ return status
50
+
51
+ def refresh_status(self):
52
+ self._status = None
53
+ return self.status
54
+
55
+ def get(self, url, params=None, **kwargs):
56
+ result = requests.get(self.url + url, params=params, **kwargs)
57
+ result.raise_for_status()
58
+ return result
59
+
60
+ def post(self, url, data=None, json=None, **kwargs):
61
+ result = requests.post(self.url + url, data=data, json=json, **kwargs)
62
+ result.raise_for_status()
63
+ return result
64
+
65
+ def __getitem__(self, item):
66
+ return self.get(item).json()
67
+
68
+ def __setitem__(self, item, value):
69
+ self.post(item, data=value)
70
+
71
+ def events(self):
72
+ stream = self.get("/api/stream", stream=True)
73
+ for line in stream.iter_lines():
74
+ if line:
75
+ line = line.decode("utf-8")
76
+ if line.startswith("data:"):
77
+ line = line[5:]
78
+ try:
79
+ yield json.loads(line)
80
+ except ValueError:
81
+ print("error", line)
82
+
83
+ def run(self):
84
+ for event in self.events():
85
+ status = self.status
86
+ name, payload = event["event"], event["payload"]
87
+ if name == "process_changed":
88
+ status["processes"][payload["uid"]].update(payload)
89
+ self._update_status_stats(status)
90
+ elif name == "notification":
91
+ self.notifications.append(payload)
92
+ event_signal = signal(name)
93
+ event_signal.send(name, payload=payload)
@@ -0,0 +1,244 @@
1
+ import fnmatch
2
+ import functools
3
+
4
+ import maya
5
+ from blinker import signal
6
+ from prompt_toolkit import PromptSession, HTML, print_formatted_text
7
+ from prompt_toolkit.application import run_in_terminal
8
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
9
+ from prompt_toolkit.completion import WordCompleter
10
+ from prompt_toolkit.history import InMemoryHistory
11
+ from prompt_toolkit.key_binding import KeyBindings
12
+ from prompt_toolkit.styles import Style
13
+ from prompt_toolkit.validation import ValidationError
14
+
15
+ from multivisor.signals import SIGNALS
16
+ from . import util
17
+
18
+ STYLE = Style.from_dict(
19
+ {
20
+ "stopped": "ansired",
21
+ "starting": "ansiblue",
22
+ "running": "ansigreen",
23
+ "backoff": "orange",
24
+ "stopping": "ansiblue",
25
+ "exited": "ansired bold",
26
+ "fatal": "violet",
27
+ "unknown": "grey",
28
+ }
29
+ )
30
+
31
+ NOTIF_STYLE = {"DEBUG": "grey", "INFO": "blue", "WARNING": "orange", "ERROR": "red"}
32
+
33
+
34
+ def process_description(process):
35
+ # TODO
36
+ status = process["statename"]
37
+ if status == "FATAL":
38
+ return process["description"]
39
+ elif process["running"]:
40
+ start = maya.MayaDT(process["start"])
41
+ desc = "pid {pid}, started {start} ({delta})".format(
42
+ pid=process["pid"], start=start.rfc2822(), delta=start.slang_time()
43
+ )
44
+ else:
45
+ stop = maya.MayaDT(process["stop"])
46
+ desc = "stopped on {stop} ({delta} ago)".format(
47
+ stop=stop.rfc2822(), delta=stop.slang_time()
48
+ )
49
+ return desc
50
+
51
+
52
+ def process_status(process, max_puid_len=10, group_by="group"):
53
+ state = process["statename"]
54
+ uid = u"{{uid:{}}}".format(max_puid_len).format(uid=process["uid"])
55
+ desc = process_description(process)
56
+ text = u"{p}{uid} <{lstate}>{state:8}</{lstate}> {description}".format(
57
+ p=("" if group_by in (None, "process") else " "),
58
+ uid=uid,
59
+ state=state,
60
+ lstate=state.lower(),
61
+ description=desc,
62
+ )
63
+ return HTML(text)
64
+
65
+
66
+ def processes_status(status, group_by="group", filter="*"):
67
+ filt = lambda p: fnmatch.fnmatch(p["uid"], filter)
68
+ return util.processes_status(
69
+ status, group_by=group_by, process_filter=filt, process_status=process_status
70
+ )
71
+
72
+
73
+ def print_processes_status(status, *args):
74
+ kwargs = {}
75
+ if args:
76
+ kwargs["filter"] = args[0]
77
+ for text in processes_status(status, **kwargs):
78
+ print_formatted_text(text, style=STYLE)
79
+
80
+
81
+ def cmd(f=None, name=None):
82
+ if f is None:
83
+ return functools.partial(cmd, name=name)
84
+ name = name or f.__name__
85
+ f.__cmd__ = name.decode() if isinstance(name, bytes) else name
86
+ return f
87
+
88
+
89
+ class Commands(object):
90
+ def __init__(self, multivisor):
91
+ self.multivisor = multivisor
92
+
93
+ @cmd(name="refresh-status")
94
+ def refresh_status(self):
95
+ """
96
+ refresh Refresh status (eq of Ctrl+F5 in browser)
97
+ """
98
+ print_processes_status(self.multivisor.refresh_status())
99
+
100
+ @cmd
101
+ def status(self, *args):
102
+ """
103
+ status Status of all processes
104
+ status <pattern> Status of a pattern of processes
105
+ """
106
+ print_processes_status(self.multivisor.status, *args)
107
+
108
+ @cmd
109
+ def restart(self, *args):
110
+ """
111
+ restart <pattern> <pattern>* Restart a list of process patterns
112
+ """
113
+ if not args:
114
+ raise ValidationError(message="Need at least one process")
115
+ self.multivisor.restart_processes(*args)
116
+
117
+ @cmd
118
+ def stop(self, *args):
119
+ """
120
+ stop <pattern> <pattern>* Stop a list of process patterns
121
+ """
122
+ if not args:
123
+ raise ValidationError(message="Need at least one process")
124
+ self.multivisor.stop_processes(*args)
125
+
126
+ @cmd
127
+ def help(self, *args):
128
+ """
129
+ Available commands (type help <topic>):
130
+ =======================================
131
+
132
+ {cmds}
133
+ """
134
+ if not args:
135
+ args = ("help",)
136
+ cmd = self.get_command(args[0])
137
+ cmds = " ".join(self.get_commands())
138
+ raw_text = cmd.__doc__.format(cmds=cmds)
139
+ text = "\n".join(map(str.strip, raw_text.split("\n")))
140
+ print_formatted_text(text)
141
+
142
+ @classmethod
143
+ def get_commands(cls):
144
+ result = {}
145
+ for name in dir(cls):
146
+ member = getattr(cls, name)
147
+ cmd = getattr(member, "__cmd__", None)
148
+ if cmd:
149
+ result[cmd] = name
150
+ return result
151
+
152
+ def get_command(self, name):
153
+ method_name = self.get_commands().get(name)
154
+ if method_name is None:
155
+ raise ValidationError(message="Unknown command '{}'".format(name))
156
+ return getattr(self, method_name)
157
+
158
+
159
+ def Prompt(**kwargs):
160
+ history = InMemoryHistory()
161
+ auto_suggest = AutoSuggestFromHistory()
162
+ prmpt = u"multivisor> "
163
+ return PromptSession(prmpt, history=history, auto_suggest=auto_suggest, **kwargs)
164
+
165
+
166
+ class Repl(object):
167
+
168
+ keys = KeyBindings()
169
+
170
+ def __init__(self, multivisor):
171
+ self.multivisor = multivisor
172
+ self.commands = Commands(multivisor)
173
+ status = self.multivisor.status
174
+ words = list(status["processes"].keys())
175
+ words.extend(self.commands.get_commands())
176
+ completer = WordCompleter(words)
177
+ self.session = Prompt(
178
+ completer=completer, bottom_toolbar=self.toolbar, key_bindings=self.keys
179
+ )
180
+ self.session.app.commands = self.commands
181
+ self.__update_toolbar()
182
+ for signal_name in SIGNALS:
183
+ signal(signal_name).connect(self.__update_toolbar)
184
+
185
+ def __update_toolbar(self, *args, **kwargs):
186
+ status = self.multivisor.status
187
+ stats = status["stats"]
188
+ s_stats, p_stats = stats["supervisors"], stats["processes"]
189
+ notifications = self.multivisor.notifications
190
+ if notifications:
191
+ notif = notifications[-1]
192
+ else:
193
+ notif = dict(level="INFO", message="Welcome to multivisor CLI")
194
+ html = (
195
+ u"{name} | Supervisors: {s[total]} ("
196
+ u'<b><style bg="green">{s[running]}</style></b>/'
197
+ u'<b><style bg="red">{s[stopped]}</style></b>) '
198
+ u"| Processes: {p[total]} ("
199
+ u'<b><style bg="green">{p[running]}</style></b>/'
200
+ u'<b><style bg="red">{p[stopped]}</style></b>) '
201
+ u'| <style bg="{notif_color}">{notif_msg}</style>'.format(
202
+ name=status["name"],
203
+ s=s_stats,
204
+ p=p_stats,
205
+ notif_color=NOTIF_STYLE[notif["level"]],
206
+ notif_msg=notif["message"],
207
+ )
208
+ )
209
+ self.__toolbar = HTML(html)
210
+ self.session.app.invalidate()
211
+
212
+ @keys.add(u"f5")
213
+ def __on_refresh(self):
214
+ run_in_terminal(self.app.commands.refresh_status())
215
+
216
+ def parse_command_line(self, text):
217
+ args = text.split()
218
+ cmd = self.commands.get_command(args[0])
219
+ return cmd, args[1:]
220
+
221
+ def run_command_line(self, text):
222
+ try:
223
+ cmd, args = self.parse_command_line(text)
224
+ cmd(*args)
225
+ except KeyboardInterrupt:
226
+ raise
227
+ except Exception as err:
228
+ print_formatted_text(HTML(u"<red>Error:</red> {}".format(err)))
229
+
230
+ def toolbar(self):
231
+ return self.__toolbar
232
+
233
+ def run(self):
234
+ self.commands.status()
235
+ while True:
236
+ try:
237
+ text = self.session.prompt()
238
+ if not text:
239
+ continue
240
+ self.run_command_line(text)
241
+ except KeyboardInterrupt:
242
+ continue
243
+ except EOFError:
244
+ break
@@ -0,0 +1,58 @@
1
+ import collections
2
+
3
+
4
+ def group_processes_status_by(processes, group_by="group", process_filter=None):
5
+ result = collections.defaultdict(lambda: dict(processes={}))
6
+ if process_filter is None:
7
+ process_filter = lambda p: True
8
+ for uid, process in processes.items():
9
+ if not process_filter(process):
10
+ continue
11
+ name = process[group_by]
12
+ order = result[name]
13
+ order["name"] = name
14
+ order["processes"][uid] = process
15
+ return result
16
+
17
+
18
+ def default_process_status(process, max_puid_len=10, group_by="group"):
19
+ nuid = "{{uid:{}}}".format(max_puid_len).format(uid=process["uid"])
20
+ if group_by in (None, "process"):
21
+ template = "{nuid} {statename:8} {description}"
22
+ else:
23
+ template = " {nuid} {statename:8} {description}"
24
+ return template.format(nuid=nuid, **process)
25
+
26
+
27
+ def processes_status(
28
+ status,
29
+ group_by="process",
30
+ process_filter=None,
31
+ process_status=default_process_status,
32
+ ):
33
+ processes = status["processes"]
34
+ if processes:
35
+ puid_len = max(map(len, processes))
36
+ else:
37
+ puid_len = 8
38
+ result = []
39
+ if process_filter is None:
40
+ process_filter = lambda p: True
41
+ if group_by in (None, "process"):
42
+ for puid in sorted(processes):
43
+ process = processes[puid]
44
+ if process_filter(process):
45
+ result.append(
46
+ process_status(process, max_puid_len=puid_len, group_by=group_by)
47
+ )
48
+ else:
49
+ grouped = group_processes_status_by(
50
+ processes, group_by=group_by, process_filter=process_filter
51
+ )
52
+ for name in sorted(grouped):
53
+ result.append(name + ":")
54
+ for process in grouped[name]["processes"].values():
55
+ result.append(
56
+ process_status(process, max_puid_len=puid_len, group_by=group_by)
57
+ )
58
+ return result