manage-iocs 0.1.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.
@@ -0,0 +1,11 @@
1
+ """Top level API.
2
+
3
+ .. data:: __version__
4
+ :type: str
5
+
6
+ Version number as calculated by https://github.com/pypa/setuptools_scm
7
+ """
8
+
9
+ from ._version import __version__
10
+
11
+ __all__ = ["__version__"]
@@ -0,0 +1,38 @@
1
+ """Interface for ``python -m manage_iocs``."""
2
+
3
+ import inspect
4
+ import sys
5
+ from collections.abc import Callable
6
+
7
+ from . import commands
8
+
9
+
10
+ def get_command_from_args(args: list[str]) -> Callable:
11
+ if len(args) < 2:
12
+ raise RuntimeError("No command provided!")
13
+
14
+ command = getattr(commands, args[1], None)
15
+ if not command or not inspect.isfunction(command):
16
+ raise RuntimeError(f"Unknown command: {args[1]}")
17
+
18
+ if not bool(inspect.signature(command).parameters):
19
+ return command
20
+ elif len(args) < 3:
21
+ raise RuntimeError(f"Command '{command.__name__}' requires additional arguments!")
22
+
23
+ # Return a lambda that calls the command with the additional args
24
+ # Assign it the same name as the original command for testing purposes
25
+ def command_w_args():
26
+ return command(*args[2:])
27
+
28
+ command_w_args.__name__ = command.__name__
29
+
30
+ return command_w_args
31
+
32
+
33
+ def main():
34
+ get_command_from_args(sys.argv)()
35
+
36
+
37
+ if __name__ == "__main__":
38
+ sys.exit(main())
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,387 @@
1
+ import inspect
2
+ import socket
3
+ import sys
4
+ import time as ttime
5
+ from subprocess import PIPE, Popen
6
+
7
+ from . import __version__, utils
8
+
9
+ EXTRA_PAD_WIDTH = 5
10
+
11
+ # Length added to string to account for ANSI color escape codes
12
+ ANSI_COLOR_ESC_CODE_LEN = 9
13
+
14
+
15
+ def version():
16
+ """Print the version of the manage-iocs package."""
17
+
18
+ print(f"Version: {__version__}")
19
+ return 0
20
+
21
+
22
+ def help():
23
+ """Display this help message."""
24
+ version()
25
+ print("Usage: manage-iocs [command] <ioc>")
26
+ print("Available commands:")
27
+ docs: dict[str, str] = {}
28
+ signatures: dict[str, list[str]] = {}
29
+ for func in inspect.getmembers(sys.modules[__name__], inspect.isfunction):
30
+ docs[func[0]] = str(func[1].__doc__)
31
+ signatures[func[0]] = list(inspect.signature(func[1]).parameters.keys())
32
+
33
+ usages = [
34
+ f" {name} " + " ".join(f"<{param}>" for param in params)
35
+ for name, params in signatures.items()
36
+ ]
37
+ max_sig_len = max(len(sig) for sig in usages)
38
+
39
+ for usage, doc in zip(usages, docs.values(), strict=False):
40
+ print(f" {usage.ljust(max_sig_len + EXTRA_PAD_WIDTH)} - {doc}")
41
+ return 0
42
+
43
+
44
+ @utils.requires_ioc_installed
45
+ def attach(ioc: str):
46
+ """Connect to procServ telnet server for the given IOC."""
47
+
48
+ is_running, _ = utils.get_ioc_status(ioc)
49
+ if is_running != "Running":
50
+ raise RuntimeError(f"Cannot attach to IOC '{ioc}': IOC is not running!")
51
+
52
+ procserv_port = utils.get_ioc_procserv_port(ioc)
53
+ print(f"Attaching to IOC '{ioc}' at port {procserv_port}...")
54
+ proc = Popen(
55
+ ["telnet", "localhost", str(procserv_port)],
56
+ stdin=PIPE,
57
+ stdout=PIPE,
58
+ )
59
+ return proc.wait()
60
+
61
+
62
+ def report():
63
+ """Show config(s) of an all IOCs on localhost"""
64
+ base_hostname = socket.gethostname()
65
+ if "." in base_hostname:
66
+ base_hostname = base_hostname.split(".")[0]
67
+
68
+ iocs = [
69
+ ioc_config
70
+ for ioc_config in utils.find_iocs().values()
71
+ if ioc_config.host == "localhost"
72
+ or ioc_config.host == base_hostname
73
+ or ioc_config.host == socket.gethostname()
74
+ ]
75
+
76
+ if len(iocs) == 0:
77
+ print("No IOCs found on configured to run on this host.")
78
+ print(f"Searched in: {utils.IOC_SEARCH_PATH}")
79
+ return 1
80
+
81
+ if len({ioc.procserv_port for ioc in iocs}) < len(iocs):
82
+ print("Warning: Detected multiple IOCs configured to use the same procServ port!")
83
+ elif len({ioc.name for ioc in iocs}) < len(iocs):
84
+ print("Warning: Detected multiple IOCs configured with the same name!")
85
+
86
+ max_base_len = max(len(str(ioc.path)) for ioc in iocs) + EXTRA_PAD_WIDTH
87
+ max_ioc_name_len = max(len(ioc.name) for ioc in iocs) + EXTRA_PAD_WIDTH
88
+ max_user_len = max(len(ioc.user) for ioc in iocs) + EXTRA_PAD_WIDTH
89
+ max_port_len = max(len(str(ioc.procserv_port)) for ioc in iocs) + EXTRA_PAD_WIDTH
90
+ max_exec_len = max(len(ioc.exec_path) for ioc in iocs) + max_base_len - EXTRA_PAD_WIDTH
91
+
92
+ header = (
93
+ f"{'BASE'.ljust(max_base_len)}| {'IOC'.ljust(max_ioc_name_len)}| "
94
+ f"{'USER'.ljust(max_user_len)}| {'PORT'.ljust(max_port_len)}| "
95
+ f"{'EXEC'.ljust(max_exec_len)}"
96
+ )
97
+ print(header)
98
+ print("-" * len(header))
99
+ for ioc in iocs:
100
+ ttime.sleep(0.01)
101
+ print(
102
+ f"{str(ioc.path).ljust(max_base_len)}| {ioc.name.ljust(max_ioc_name_len)}| "
103
+ f"{ioc.user.ljust(max_user_len)}| {str(ioc.procserv_port).ljust(max_port_len)}| "
104
+ f"{str(ioc.path / ioc.chdir / ioc.exec_path).ljust(max_exec_len)}"
105
+ )
106
+
107
+
108
+ @utils.requires_root
109
+ @utils.requires_ioc_installed
110
+ def disable(ioc: str):
111
+ """Disable autostart for the given IOC."""
112
+
113
+ _, _, ret = utils.systemctl_passthrough("disable", ioc)
114
+ if ret == 0:
115
+ print(f"Autostart disabled for IOC '{ioc}'")
116
+ else:
117
+ raise RuntimeError(f"Failed to disable autostart for IOC '{ioc}'!")
118
+ return ret
119
+
120
+
121
+ @utils.requires_root
122
+ @utils.requires_ioc_installed
123
+ def enable(ioc: str):
124
+ """Enable autostart for the given IOC."""
125
+
126
+ _, _, ret = utils.systemctl_passthrough("enable", ioc)
127
+ if ret == 0:
128
+ print(f"Autostart enabled for IOC '{ioc}'")
129
+ else:
130
+ raise RuntimeError(f"Failed to enable autostart for IOC '{ioc}'!")
131
+ return ret
132
+
133
+
134
+ @utils.requires_ioc_installed
135
+ def start(ioc: str):
136
+ """Start the given IOC."""
137
+
138
+ _, _, ret = utils.systemctl_passthrough("start", ioc)
139
+ if ret == 0:
140
+ print(f"IOC '{ioc}' started successfully.")
141
+ else:
142
+ raise RuntimeError(f"Failed to start IOC '{ioc}'!")
143
+ return ret
144
+
145
+
146
+ def startall():
147
+ """Start all IOCs on this host."""
148
+
149
+ iocs = utils.find_installed_iocs().values()
150
+ ret = 0
151
+ for ioc in iocs:
152
+ ret += start(ioc.name)
153
+ return ret
154
+
155
+
156
+ @utils.requires_ioc_installed
157
+ def stop(ioc: str):
158
+ """Stop the given IOC."""
159
+
160
+ _, _, ret = utils.systemctl_passthrough("stop", ioc)
161
+ if ret == 0:
162
+ print(f"IOC '{ioc}' stopped successfully.")
163
+ else:
164
+ raise RuntimeError(f"Failed to stop IOC '{ioc}'!")
165
+ return ret
166
+
167
+
168
+ def stopall():
169
+ """Stop all IOCs on this host."""
170
+
171
+ iocs = utils.find_installed_iocs().values()
172
+ ret = 0
173
+ for ioc in iocs:
174
+ ret += stop(ioc.name)
175
+ return ret
176
+
177
+
178
+ @utils.requires_root
179
+ def enableall():
180
+ """Enable autostart for all IOCs on this host."""
181
+
182
+ iocs = utils.find_installed_iocs().values()
183
+ ret = 0
184
+ for ioc in iocs:
185
+ ret += enable(ioc.name)
186
+ return ret
187
+
188
+
189
+ @utils.requires_root
190
+ def disableall():
191
+ """Disable autostart for all IOCs on this host."""
192
+
193
+ iocs = utils.find_installed_iocs().values()
194
+ ret = 0
195
+ for ioc in iocs:
196
+ ret += disable(ioc.name)
197
+ return ret
198
+
199
+
200
+ @utils.requires_ioc_installed
201
+ def restart(ioc: str):
202
+ """Restart the given IOC."""
203
+
204
+ _, _, ret = utils.systemctl_passthrough("restart", ioc)
205
+ if ret == 0:
206
+ print(f"IOC '{ioc}' restarted successfully.")
207
+ else:
208
+ raise RuntimeError(f"Failed to restart IOC '{ioc}'!")
209
+ return ret
210
+
211
+
212
+ @utils.requires_root
213
+ def uninstall(ioc: str):
214
+ """Remove /etc/systemd/system/softioc-[ioc].service"""
215
+
216
+ _, _, ret = utils.systemctl_passthrough("stop", ioc)
217
+ if ret != 0:
218
+ raise RuntimeError(f"Failed to stop IOC '{ioc}' before uninstalling!")
219
+ _, _, ret = utils.systemctl_passthrough("disable", ioc)
220
+ if ret != 0:
221
+ raise RuntimeError(f"Failed to disable IOC '{ioc}' before uninstalling!")
222
+ _, _, ret = utils.systemctl_passthrough("uninstall", ioc)
223
+ if ret == 0:
224
+ print(f"IOC '{ioc}' uninstalled successfully.")
225
+ else:
226
+ raise RuntimeError(f"Failed to uninstall IOC '{ioc}'!")
227
+ return ret
228
+
229
+
230
+ @utils.requires_root
231
+ def install(ioc: str):
232
+ """Create /etc/systemd/system/softioc-[ioc].service"""
233
+
234
+ iocs = utils.find_installed_iocs()
235
+ if ioc in iocs:
236
+ raise RuntimeError(f"IOC '{ioc}' is already installed!")
237
+
238
+ procserv_ports = [ioc.procserv_port for ioc in utils.find_installed_iocs().values()]
239
+ if utils.find_iocs()[ioc].procserv_port in procserv_ports:
240
+ raise RuntimeError(
241
+ f"Cannot install IOC '{ioc}': procServ port "
242
+ f"{utils.find_iocs()[ioc].procserv_port} is already in use!"
243
+ )
244
+
245
+ service_file = utils.SYSTEMD_SERVICE_PATH / f"softioc-{ioc}.service"
246
+ ioc_config = utils.find_iocs()[ioc]
247
+ base_hostname = socket.gethostname()
248
+ if "." in base_hostname:
249
+ base_hostname = base_hostname.split(".")[0]
250
+
251
+ if ioc_config.host not in [base_hostname, "localhost", socket.gethostname()]:
252
+ raise RuntimeError(
253
+ f"Cannot install IOC '{ioc}' on this host; configured host is '{ioc_config.host}'!"
254
+ )
255
+
256
+ if ioc_config.user == "root":
257
+ raise RuntimeError(f"Refusing to install IOC '{ioc}' to run as user 'root'!")
258
+
259
+ with open(service_file, "w") as f:
260
+ f.write(
261
+ f"""
262
+ #
263
+ # Installed by manage-iocs
264
+ #
265
+ [Unit]
266
+ Description=IOC {ioc} via procServ
267
+ After=network.target remote_fs.target local_fs.target syslog.target time.target centrifydc.service
268
+ ConditionFileIsExecutable=/usr/bin/procServ
269
+
270
+ [Service]
271
+ User={ioc_config.user}
272
+ ExecStart=/usr/bin/procServ -f -q -c {ioc_config.path} -i ^D^C^] -p /var/run/softioc-{ioc}.pid \
273
+ -n {ioc} --restrict -L /var/log/softioc/{ioc}/{ioc}.log \
274
+ {ioc_config.procserv_port} {ioc_config.path}/{ioc_config.exec_path}
275
+ Environment="PROCPORT={ioc_config.procserv_port}"
276
+ Environment="HOSTNAME={ioc_config.host}"
277
+ Environment="IOCNAME={ioc}"
278
+ Environment="TOP={ioc_config.path}"
279
+ #Restart=on-failure
280
+
281
+ [Install]
282
+ WantedBy=multi-user.target
283
+ """
284
+ )
285
+
286
+ _, stderr, ret = utils.systemctl_passthrough("install", ioc)
287
+ if ret == 0:
288
+ print(f"IOC '{ioc}' installed successfully.")
289
+ else:
290
+ raise RuntimeError(f"Failed to install IOC '{ioc}'!: {stderr.strip()}")
291
+ return ret
292
+
293
+
294
+ def status():
295
+ """Get the status of the given IOC."""
296
+
297
+ ret = 0
298
+ statuses: dict[str, tuple[str, bool]] = {}
299
+ installed_iocs = utils.find_installed_iocs().keys()
300
+ if len(installed_iocs) == 0:
301
+ print("No Installed IOCs found on this host.")
302
+ return 1
303
+
304
+ for installed_ioc in installed_iocs:
305
+ try:
306
+ statuses[installed_ioc] = utils.get_ioc_status(installed_ioc)
307
+ except RuntimeError:
308
+ pass # TODO: Handle this better?
309
+
310
+ max_ioc_name_len = max(len(ioc_name) for ioc_name in statuses.keys()) + EXTRA_PAD_WIDTH
311
+ max_status_len = max(len(status[0]) for status in statuses.values()) + EXTRA_PAD_WIDTH
312
+ max_enabled_len = len("Auto-Start")
313
+
314
+ print(f"{'IOC'.ljust(max_ioc_name_len)}{'Status'.ljust(max_status_len)}Auto-Start")
315
+ ttime.sleep(0.01)
316
+ print("-" * (max_ioc_name_len + max_status_len + max_enabled_len))
317
+ for ioc_name, (state, is_enabled) in statuses.items():
318
+ ttime.sleep(0.01)
319
+ if state == "Running":
320
+ state_str = f"\033[92m{state}\033[0m" # Green
321
+ elif state == "Stopped":
322
+ state_str = f"\033[91m{state}\033[0m" # Red
323
+ else:
324
+ state_str = f"\033[93m{state}\033[0m" # Yellow
325
+ print(
326
+ f"{ioc_name.ljust(max_ioc_name_len)}{state_str.ljust(max_status_len + ANSI_COLOR_ESC_CODE_LEN)}{'Enabled' if is_enabled else 'Disabled'}" # noqa: E501
327
+ )
328
+
329
+ return ret
330
+
331
+
332
+ def nextport():
333
+ """Find the next unused procServ port."""
334
+
335
+ used_ports = [ioc.procserv_port for ioc in utils.find_iocs().values()]
336
+
337
+ print(max(used_ports) + 1 if len(used_ports) > 0 else 4000)
338
+ return 0
339
+
340
+
341
+ @utils.requires_ioc_installed
342
+ def lastlog(ioc: str):
343
+ """Display the output of the last IOC startup"""
344
+
345
+ log_file = utils.MANAGE_IOCS_LOG_PATH / f"{ioc}.log"
346
+ if not log_file.exists():
347
+ raise RuntimeError(f"No log file found for IOC '{ioc}' at '{log_file}'!")
348
+
349
+ with open(log_file) as f:
350
+ log = f.readlines()
351
+ start_index = 0
352
+ for i, line in reversed(list(enumerate(log))):
353
+ if line.strip() == f'@@@ Restarting child "{ioc}"':
354
+ start_index = i
355
+ break
356
+ if start_index == 0:
357
+ last_log = "".join(log)
358
+ else:
359
+ last_log = "".join(log[start_index:])
360
+ print(last_log)
361
+ return 0
362
+
363
+
364
+ @utils.requires_ioc_installed
365
+ @utils.requires_root
366
+ def rename(ioc: str, new_name: str):
367
+ """Rename an installed IOC."""
368
+
369
+ state, is_enabled = utils.get_ioc_status(ioc)
370
+ uninstall(ioc)
371
+
372
+ ioc_config = utils.find_iocs()[ioc]
373
+ with open(ioc_config.path / "config", "w") as f:
374
+ f.write(f"NAME={new_name}\n")
375
+ f.write(f"HOST={ioc_config.host}\n")
376
+ f.write(f"PORT={ioc_config.procserv_port}\n")
377
+ f.write(f"USER={ioc_config.user}\n")
378
+ if ioc_config.exec_path:
379
+ f.write(f"EXEC={ioc_config.exec_path}\n")
380
+ if ioc_config.chdir and len(ioc_config.chdir) > 0:
381
+ f.write(f"CHDIR={ioc_config.chdir}\n")
382
+
383
+ install(new_name)
384
+ if is_enabled:
385
+ enable(new_name)
386
+ if state == "Running":
387
+ start(new_name)
manage_iocs/utils.py ADDED
@@ -0,0 +1,136 @@
1
+ import functools
2
+ import os
3
+ import socket
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from subprocess import PIPE, Popen
8
+
9
+ IOC_SEARCH_PATH = [Path("/epics/iocs"), Path("/opt/epics/iocs"), Path("/opt/iocs")]
10
+ if "MANAGE_IOCS_SEARCH_PATH" in os.environ:
11
+ IOC_SEARCH_PATH.extend(
12
+ [Path(p) for p in os.environ["MANAGE_IOCS_SEARCH_PATH"].split(os.pathsep)]
13
+ )
14
+
15
+ SYSTEMD_SERVICE_PATH = Path("/etc/systemd/system")
16
+ MANAGE_IOCS_LOG_PATH = Path("/var/log/softioc")
17
+
18
+
19
+ @dataclass
20
+ class IOC:
21
+ name: str
22
+ user: str
23
+ procserv_port: int
24
+ path: Path
25
+ host: str
26
+ exec_path: str
27
+ chdir: str
28
+
29
+
30
+ def read_config_file(config_path: Path) -> dict[str, str]:
31
+ """Read config file for IOC"""
32
+ config: dict[str, str] = {}
33
+ with open(config_path) as f:
34
+ for line in f:
35
+ line = line.strip()
36
+ if line and not line.startswith("#"):
37
+ key, value = line.split("=", 1)
38
+ config[key.strip()] = value.strip()
39
+ return config
40
+
41
+
42
+ def find_iocs() -> dict[str, IOC]:
43
+ """Get a list of IOCs available in the search paths."""
44
+ iocs = {}
45
+ search_paths = IOC_SEARCH_PATH
46
+ for search_path in search_paths:
47
+ if os.path.exists(search_path):
48
+ for item in os.listdir(search_path):
49
+ if os.path.isdir(search_path / item) and os.path.exists(
50
+ search_path / item / "config"
51
+ ):
52
+ config = read_config_file(search_path / item / "config")
53
+ iocs[item] = IOC(
54
+ name=item,
55
+ procserv_port=int(config["PORT"]),
56
+ path=search_path / item,
57
+ host=config.get("HOST", "localhost"),
58
+ user=config.get("USER", "iocuser"),
59
+ exec_path=config.get("EXEC", "st.cmd"),
60
+ chdir=config.get("CHDIR", "."),
61
+ )
62
+ return iocs
63
+
64
+
65
+ def find_iocs_on_host() -> dict[str, IOC]:
66
+ """Get a list of IOCs available on the given host."""
67
+ all_iocs = find_iocs()
68
+ return {
69
+ name: ioc
70
+ for name, ioc in all_iocs.items()
71
+ if ioc.host == socket.gethostname() or ioc.host == "localhost"
72
+ }
73
+
74
+
75
+ def find_installed_iocs() -> dict[str, IOC]:
76
+ """Get a list of IOCs that have systemd service files installed."""
77
+ iocs = {}
78
+ for ioc in find_iocs().values():
79
+ service_file = SYSTEMD_SERVICE_PATH / f"softioc-{ioc.name}.service"
80
+ if service_file.exists():
81
+ iocs[ioc.name] = ioc
82
+ return iocs
83
+
84
+
85
+ def get_ioc_procserv_port(ioc: str) -> int:
86
+ """Get the procServ port number for the given IOC."""
87
+
88
+ return find_iocs()[ioc].procserv_port
89
+
90
+
91
+ def systemctl_passthrough(action: str, ioc: str) -> tuple[str, str, int]:
92
+ """Helper to call systemctl with the given action and IOC name."""
93
+ proc = Popen(["systemctl", action, f"softioc-{ioc}.service"], stdin=PIPE, stdout=PIPE)
94
+ out, err = proc.communicate()
95
+ decoded_out = out.decode().strip() if out else ""
96
+ decoded_err = err.decode().strip() if err else ""
97
+ return decoded_out, decoded_err, proc.returncode
98
+
99
+
100
+ def get_ioc_status(ioc_name: str) -> tuple[str, bool]:
101
+ """Get the active and enabled status of the given IOC."""
102
+
103
+ state, err, _ = systemctl_passthrough("is-active", ioc_name)
104
+
105
+ # Convert to more user-friendly terms
106
+ if state == "active":
107
+ state = "Running"
108
+ elif state == "inactive":
109
+ state = "Stopped"
110
+
111
+ enabled, err, _ = systemctl_passthrough("is-enabled", ioc_name)
112
+ if enabled not in ("enabled", "disabled"):
113
+ raise RuntimeError(err)
114
+
115
+ return state.capitalize(), enabled == "enabled"
116
+
117
+
118
+ def requires_root(func: Callable):
119
+ @functools.wraps(func)
120
+ def wrapper(*args):
121
+ if os.geteuid() != 0:
122
+ raise PermissionError(f"Command {func.__name__} requires root privileges.")
123
+ return func(*args)
124
+
125
+ return wrapper
126
+
127
+
128
+ def requires_ioc_installed(func: Callable):
129
+ @functools.wraps(func)
130
+ def wrapper(ioc: str, *args, **kwargs):
131
+ installed_iocs = find_installed_iocs()
132
+ if ioc not in installed_iocs:
133
+ raise RuntimeError(f"No IOC with name '{ioc}' is installed!")
134
+ return func(ioc, *args, **kwargs)
135
+
136
+ return wrapper
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: manage-iocs
3
+ Version: 0.1.0
4
+ Summary: CLI utility for auto-creating phoebus engineering screens from EPICS db templates
5
+ Author-email: Jakub Wlodek <jwlodek@bnl.gov>
6
+ License: BSD 3-Clause License
7
+
8
+ Copyright (c) 2025, Brookhaven National Laboratory
9
+
10
+ Redistribution and use in source and binary forms, with or without
11
+ modification, are permitted provided that the following conditions are met:
12
+
13
+ 1. Redistributions of source code must retain the above copyright notice, this
14
+ list of conditions and the following disclaimer.
15
+
16
+ 2. Redistributions in binary form must reproduce the above copyright notice,
17
+ this list of conditions and the following disclaimer in the documentation
18
+ and/or other materials provided with the distribution.
19
+
20
+ 3. Neither the name of the copyright holder nor the names of its
21
+ contributors may be used to endorse or promote products derived from
22
+ this software without specific prior written permission.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34
+
35
+ Project-URL: GitHub, https://github.com/NSLS2/manage-iocs
36
+ Classifier: Development Status :: 3 - Alpha
37
+ Classifier: License :: OSI Approved :: Apache Software License
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Requires-Python: >=3.11
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Requires-Dist: pyyaml
45
+ Provides-Extra: dev
46
+ Requires-Dist: copier; extra == "dev"
47
+ Requires-Dist: pipdeptree; extra == "dev"
48
+ Requires-Dist: pre-commit; extra == "dev"
49
+ Requires-Dist: pyright; extra == "dev"
50
+ Requires-Dist: pytest; extra == "dev"
51
+ Requires-Dist: pytest-cov; extra == "dev"
52
+ Requires-Dist: ruff; extra == "dev"
53
+ Requires-Dist: tox-direct; extra == "dev"
54
+ Requires-Dist: types-mock; extra == "dev"
55
+ Requires-Dist: import-linter; extra == "dev"
56
+ Dynamic: license-file
57
+
58
+ # Manage-IOCs [![CI](https://github.com/NSLS2/manage-iocs/actions/workflows/ci.yml/badge.svg)](https://github.com/NSLS2/manage-iocs/actions/workflows/ci.yml)
59
+
60
+ A re-write of the `manage-iocs` utility in pure python.
@@ -0,0 +1,11 @@
1
+ manage_iocs/__init__.py,sha256=Ksms_WJF8LTkbm38gEpm1jBpGqcQ8NGvmb2ZJlOE1j8,198
2
+ manage_iocs/__main__.py,sha256=zdgO1if090egIEVwHFea4T73NuRIER-q8LAm1ioSyXE,1005
3
+ manage_iocs/_version.py,sha256=5jwwVncvCiTnhOedfkzzxmxsggwmTBORdFL_4wq0ZeY,704
4
+ manage_iocs/commands.py,sha256=wsvKlZ6xEj_JWyqfVg4lK1ovP4lpr1N4aoeKw3W5cWw,11874
5
+ manage_iocs/utils.py,sha256=gAQVC5F908LHxQ32wp_BXpUieZnbtrT2I9b7TYN55Fc,4309
6
+ manage_iocs-0.1.0.dist-info/licenses/LICENSE,sha256=kF_ROu6b6TScdLaB1UbUhxnQAya6_eS90L3YJWt8dlc,1545
7
+ manage_iocs-0.1.0.dist-info/METADATA,sha256=DgPZHuEfeVJG98U2Xq5VJ1YFHe8HXs2RIXzT557kA_c,3032
8
+ manage_iocs-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ manage_iocs-0.1.0.dist-info/entry_points.txt,sha256=GtfxlU1jaRqK2R3Zd7kux1m1ySTiMk6ftaElbbKUEuE,58
10
+ manage_iocs-0.1.0.dist-info/top_level.txt,sha256=ZWSNEmez5G5Em-4EPOM83S1ypgwVHDOFl4YVpt3r2-E,12
11
+ manage_iocs-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ manage-iocs = manage_iocs.__main__:main
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Brookhaven National Laboratory
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ manage_iocs