PyroLab 0.3.2__py3-none-any.whl → 0.4.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 (44) hide show
  1. pyrolab/__init__.py +56 -7
  2. pyrolab/api.py +6 -6
  3. pyrolab/cli.py +220 -98
  4. pyrolab/configure.py +109 -71
  5. pyrolab/drivers/VISAResourceExtensions.py +65 -34
  6. pyrolab/drivers/__init__.py +20 -19
  7. pyrolab/drivers/arduino/arduino.py +28 -13
  8. pyrolab/drivers/cameras/amcam.py +61 -0
  9. pyrolab/drivers/cameras/sciTSI.py +39 -32
  10. pyrolab/drivers/cameras/thorcam.py +119 -68
  11. pyrolab/drivers/cameras/uc480.py +66 -60
  12. pyrolab/drivers/fpgas/__init__.py +16 -0
  13. pyrolab/drivers/fpgas/analogdiscovery.py +310 -0
  14. pyrolab/drivers/function_generators/__init__.py +16 -0
  15. pyrolab/drivers/function_generators/smcv100b.py +389 -0
  16. pyrolab/drivers/lasers/ppcl55x.py +259 -233
  17. pyrolab/drivers/lasers/sachertec.py +25 -20
  18. pyrolab/drivers/lasers/tsl550.py +218 -165
  19. pyrolab/drivers/motion/KMTS25E.py +92 -0
  20. pyrolab/drivers/motion/kinesis/__init__.py +9 -5
  21. pyrolab/drivers/motion/kinesis/bpc303.py +86 -42
  22. pyrolab/drivers/motion/kinesis/kdc101.py +121 -93
  23. pyrolab/drivers/motion/max31x.py +31 -23
  24. pyrolab/drivers/motion/prm1z8.py +2 -1
  25. pyrolab/drivers/motion/z8xx.py +13 -10
  26. pyrolab/drivers/sample.py +13 -8
  27. pyrolab/drivers/scopes/rohdeschwarz.py +119 -104
  28. pyrolab/drivers/smu/keysight.py +94 -83
  29. pyrolab/manager.py +138 -85
  30. pyrolab/nameserver.py +28 -16
  31. pyrolab/pyrolabd.py +37 -19
  32. pyrolab/server.py +69 -57
  33. pyrolab/service.py +13 -7
  34. pyrolab/utils.py +13 -5
  35. pyrolab-0.4.0.dist-info/METADATA +299 -0
  36. pyrolab-0.4.0.dist-info/RECORD +48 -0
  37. {pyrolab-0.3.2.dist-info → pyrolab-0.4.0.dist-info}/WHEEL +1 -1
  38. {pyrolab-0.3.2.dist-info → pyrolab-0.4.0.dist-info/licenses}/NOTICE.txt +1 -1
  39. pyrolab-0.3.2.dist-info/AUTHORS.txt +0 -20
  40. pyrolab-0.3.2.dist-info/METADATA +0 -973
  41. pyrolab-0.3.2.dist-info/RECORD +0 -43
  42. {pyrolab-0.3.2.dist-info → pyrolab-0.4.0.dist-info}/entry_points.txt +0 -0
  43. {pyrolab-0.3.2.dist-info → pyrolab-0.4.0.dist-info/licenses}/LICENSE +0 -0
  44. {pyrolab-0.3.2.dist-info → pyrolab-0.4.0.dist-info}/top_level.txt +0 -0
pyrolab/__init__.py CHANGED
@@ -27,6 +27,12 @@ import os
27
27
  import pathlib
28
28
  import platform
29
29
  import sys
30
+ import atexit
31
+ import fileinput
32
+ import re
33
+ from typing import Callable, Iterable
34
+ from time import strptime
35
+ from pathlib import Path
30
36
 
31
37
  # Check if Python version is supported
32
38
  pyversion = sys.version_info
@@ -42,11 +48,11 @@ if pyversion < (3, 7, 0):
42
48
  __name__ = "PyroLab"
43
49
  __author__ = "CamachoLab"
44
50
  __copyright__ = "Copyright 2020, The PyroLab Project"
45
- __version__ = "0.3.2"
51
+ __version__ = "0.4.0"
46
52
  __license__ = "GPLv3+"
47
53
  __maintainer__ = "Sequoia Ploeg"
48
54
  __maintainer_email__ = "sequoia.ploeg@byu.edu"
49
- __status__ = "Development" # "Production"
55
+ __status__ = "Development" # "Production"
50
56
  __project_url__ = "https://github.com/sequoiap/pyrolab"
51
57
  __forum_url__ = "https://github.com/sequoiap/pyrolab/issues"
52
58
  __website_url__ = "https://camacholab.byu.edu/"
@@ -66,6 +72,7 @@ if pyversion < (3, 9, 0):
66
72
  base_path = pathlib.Path(__file__).resolve().parent
67
73
  else:
68
74
  from importlib.resources import files
75
+
69
76
  base_path = files("pyrolab")
70
77
  base_path = base_path / "data" / "local"
71
78
 
@@ -78,6 +85,7 @@ NAMESERVER_STORAGE.mkdir(parents=True, exist_ok=True)
78
85
 
79
86
  PYROLAB_LOGDIR = PYROLAB_DATA_DIR / "logs"
80
87
  PYROLAB_LOGDIR.mkdir(parents=True, exist_ok=True)
88
+ PYROLAB_MASTERLOG = PYROLAB_LOGDIR / "pyrolab.log"
81
89
 
82
90
  LOCKFILE = PYROLAB_DATA_DIR / "pyrolabd.lock"
83
91
  USER_CONFIG_FILE = PYROLAB_DATA_DIR / "user_configuration.yaml"
@@ -98,11 +106,14 @@ def get_loglevel() -> int:
98
106
  return loglevel
99
107
 
100
108
 
101
- if len(logging.root.handlers) == 0:
109
+ if len(logging.root.handlers) == 0:
102
110
  logfile = PYROLAB_LOGDIR / f"pyrolab_{os.getpid()}.log"
103
111
  root = logging.getLogger()
104
- h = logging.handlers.RotatingFileHandler(logfile, 'a', 30000, 10)
105
- f = logging.Formatter('[%(asctime)s.%(msecs)03d] %(process)-5s %(processName)-10s %(name)-12s %(levelname)-8s %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
112
+ h = logging.handlers.RotatingFileHandler(logfile, "a", 30000, 10)
113
+ f = logging.Formatter(
114
+ "[%(asctime)s.%(msecs)03d] %(levelname)-8s %(process)-5s %(processName)-16s %(name)-20s %(message)s",
115
+ datefmt="%Y-%m-%d %H:%M:%S",
116
+ )
106
117
  h.setFormatter(f)
107
118
  root.addHandler(h)
108
119
  root.setLevel(get_loglevel())
@@ -124,10 +135,10 @@ try:
124
135
 
125
136
  url = "https://pypi.org/pypi/pyrolab/json"
126
137
  with requests.Session() as s:
127
- s.mount('https://pypi.org', HTTPAdapter(max_retries=3))
138
+ s.mount("https://pypi.org", HTTPAdapter(max_retries=3))
128
139
  resp = s.get(url).text
129
140
 
130
- v = json.loads(resp)['info']['version']
141
+ v = json.loads(resp)["info"]["version"]
131
142
  curver = parse(__version__)
132
143
  latest = parse(v)
133
144
 
@@ -139,3 +150,41 @@ try:
139
150
  log.info(message)
140
151
  except:
141
152
  pass
153
+
154
+
155
+ def try_itr(func: Callable, itr: Iterable, *exceptions, **kwargs):
156
+ """
157
+ Tests a function on an iterable, yields iterable if no exception is raised.
158
+ """
159
+ for elem in itr:
160
+ try:
161
+ func(elem, **kwargs)
162
+ yield elem
163
+ except exceptions:
164
+ pass
165
+
166
+
167
+ # @atexit.register
168
+ # def condense_logs():
169
+ # """
170
+ # Cleans up the logfile directory every once in a while.
171
+
172
+ # Runs at exit or when "pyrolab down" is called. Condenses all process log
173
+ # files into a single logfile that is sorted by timestamp.
174
+ # """
175
+ # f_names = list(PYROLAB_LOGDIR.glob("*.*"))
176
+ # lines = list(fileinput.input(f_names))
177
+ # t_fmt = "%Y-%m-%d %H:%M:%S.%f" # format of time stamps
178
+ # t_pat = re.compile(r"\[(.+?)\]") # pattern to extract timestamp
179
+ # lines = list(
180
+ # try_itr(
181
+ # lambda l: strptime(t_pat.search(l).group(1), t_fmt), lines, AttributeError
182
+ # )
183
+ # )
184
+ # with PYROLAB_MASTERLOG.open(mode="w") as f:
185
+ # for l in sorted(lines, key=lambda l: strptime(t_pat.search(l).group(1), t_fmt)):
186
+ # f.write(l)
187
+ # if PYROLAB_MASTERLOG in f_names:
188
+ # f_names.remove(PYROLAB_MASTERLOG)
189
+ # for f in f_names:
190
+ # f.unlink()
pyrolab/api.py CHANGED
@@ -14,11 +14,11 @@ from Pyro5.core import locate_ns
14
14
  from Pyro5.server import behavior, expose, oneway, serve
15
15
  from pyrolab import USER_CONFIG_FILE
16
16
  from pyrolab.configure import (
17
- PyroLabConfiguration,
18
- NameServerConfiguration,
19
- DaemonConfiguration,
20
- ServiceConfiguration,
21
- reset_config,
17
+ PyroLabConfiguration,
18
+ NameServerConfiguration,
19
+ DaemonConfiguration,
20
+ ServiceConfiguration,
21
+ reset_config,
22
22
  update_config,
23
23
  )
24
24
  from pyrolab.server import Daemon, LockableDaemon
@@ -47,7 +47,7 @@ __all__ = [
47
47
  ]
48
48
 
49
49
  # If a user config file exists, load the first listed nameserver by default,
50
- # so that locate_ns "just works."
50
+ # so that locate_ns "just works."
51
51
  if USER_CONFIG_FILE.exists():
52
52
  cfg = PyroLabConfiguration.from_file(USER_CONFIG_FILE)
53
53
  nscfg = next(iter(cfg.nameservers.values()))
pyrolab/cli.py CHANGED
@@ -20,10 +20,16 @@ import re
20
20
  from pathlib import Path
21
21
  from typing import Callable, Iterable, Optional
22
22
  from time import sleep, strptime
23
+ from tabulate import tabulate
23
24
 
24
- import pkg_resources
25
+ try:
26
+ import importlib.resources as pkg_resources
27
+ except ImportError:
28
+ import pkg_resources
25
29
  import typer
30
+ from Pyro5.errors import CommunicationError
26
31
 
32
+ import pyrolab
27
33
  from pyrolab import LOCKFILE, PYROLAB_LOGDIR, RUNTIME_CONFIG, USER_CONFIG_FILE
28
34
  from pyrolab.api import Proxy
29
35
  from pyrolab.configure import (
@@ -38,16 +44,23 @@ from pyrolab.pyrolabd import InstanceInfo, PyroLabDaemon
38
44
  def get_daemon(abort=True, suppress_reload_message=False) -> PyroLabDaemon:
39
45
  if LOCKFILE.exists():
40
46
  ii = InstanceInfo.parse_file(LOCKFILE)
41
- DAEMON = Proxy(ii.uri)
47
+ try:
48
+ DAEMON = Proxy(ii.uri)
49
+ DAEMON._pyroBind()
50
+ except CommunicationError:
51
+ raise ConnectionRefusedError("Could not connect to daemon.")
42
52
  if not suppress_reload_message and RUNTIME_CONFIG.exists():
43
53
  if RUNTIME_CONFIG.stat().st_mtime < USER_CONFIG_FILE.stat().st_mtime:
44
54
  typer.secho(
45
55
  "The configuration file has been updated. Run 'pyrolab reload' for changes to take effect.",
46
- fg=typer.colors.RED
56
+ fg=typer.colors.RED,
47
57
  )
48
58
  return DAEMON
49
59
  elif abort:
50
- typer.secho("PyroLab daemon is not running! Try 'pyrolab up' first.", fg=typer.colors.RED)
60
+ typer.secho(
61
+ "PyroLab daemon is not running! Try 'pyrolab up' first.",
62
+ fg=typer.colors.RED,
63
+ )
51
64
  raise typer.Abort()
52
65
  else:
53
66
  return None
@@ -55,7 +68,7 @@ def get_daemon(abort=True, suppress_reload_message=False) -> PyroLabDaemon:
55
68
 
56
69
  ###############################################################################
57
70
  # pyrolab Main App
58
- #
71
+ #
59
72
  # COMMANDS
60
73
  # --------
61
74
  # pyrolab up
@@ -68,29 +81,49 @@ def get_daemon(abort=True, suppress_reload_message=False) -> PyroLabDaemon:
68
81
  app = typer.Typer(no_args_is_help=True)
69
82
 
70
83
 
71
- def _version_callback(value: bool=True) -> None:
84
+ def _version_callback(value: bool = True) -> None:
72
85
  if value:
73
86
  from pyrolab import __version__
87
+
74
88
  typer.echo(f"PyroLab {__version__}")
75
89
  raise typer.Exit()
76
90
 
77
- def _show_data_dir(value: bool=True) -> None:
91
+
92
+ def _show_data_dir(value: bool = True) -> None:
78
93
  if value:
79
94
  from pyrolab import PYROLAB_DATA_DIR
95
+
80
96
  typer.echo(f"{PYROLAB_DATA_DIR}")
81
97
  raise typer.Exit()
82
98
 
99
+
83
100
  @app.callback()
84
101
  def main(
85
- version: bool = typer.Option(False, "--version", "-v", help="Show the version and exit.", callback=_version_callback, is_eager=True),
86
- show_data_dir: bool = typer.Option(False, "--data-dir", help="Show the data directories and exit.", callback=_show_data_dir, is_eager=True),
102
+ version: bool = typer.Option(
103
+ False,
104
+ "--version",
105
+ "-v",
106
+ help="Show the version and exit.",
107
+ callback=_version_callback,
108
+ is_eager=True,
109
+ ),
110
+ show_data_dir: bool = typer.Option(
111
+ False,
112
+ "--data",
113
+ "-d",
114
+ help="Show the data directories and exit.",
115
+ callback=_show_data_dir,
116
+ is_eager=True,
117
+ ),
87
118
  ):
88
119
  return
89
120
 
121
+
90
122
  @app.command()
91
123
  def up(
92
- port: int = typer.Option(None, "--port", "-p", help="Port to use for the PyroLab daemon."),
93
- force: bool = typer.Option(False, "--force", "-f", help="Force launch of the daemon (only if you're positive it has died!)."),
124
+ port: int = typer.Option(
125
+ None, "--port", "-p", help="Port to use for the PyroLab daemon."
126
+ ),
94
127
  ):
95
128
  """
96
129
  Start the background PyroLab daemon.
@@ -98,13 +131,21 @@ def up(
98
131
  Only use the `--force` flag if you're sure the daemon is dead, or you may
99
132
  orphan the process.
100
133
  """
101
- daemon = get_daemon(abort=False, suppress_reload_message=True)
134
+ try:
135
+ daemon = get_daemon(abort=False, suppress_reload_message=True)
136
+ force = False
137
+ except ConnectionRefusedError:
138
+ daemon = None
139
+ force = True
102
140
  if daemon is None or force:
103
141
  if force and LOCKFILE.exists():
104
142
  LOCKFILE.unlink()
105
-
106
- rsrc = Path(pkg_resources.resource_filename('pyrolab', "pyrolabd.py"))
107
-
143
+
144
+ try:
145
+ rsrc = pkg_resources.files(pyrolab) / "pyrolabd.py"
146
+ except AttributeError:
147
+ rsrc = Path(pkg_resources.resource_filename("pyrolab", "pyrolabd.py"))
148
+
108
149
  if port:
109
150
  args = [sys.executable, str(rsrc), str(port)]
110
151
  else:
@@ -116,15 +157,23 @@ def up(
116
157
  # same directory (certainly true for conda installations).
117
158
  executable = Path(sys.exec_prefix) / "pythonw.exe"
118
159
  args[0] = str(executable)
119
- subprocess.Popen(args, close_fds=True, start_new_session=True, creationflags=DETACHED_PROCESS)
160
+ subprocess.Popen(
161
+ args,
162
+ close_fds=True,
163
+ start_new_session=True,
164
+ creationflags=DETACHED_PROCESS,
165
+ )
120
166
  else:
121
- subprocess.Popen(args, close_fds=True, start_new_session=True) # stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
122
-
167
+ subprocess.Popen(
168
+ args, close_fds=True, start_new_session=True
169
+ ) # stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
170
+
123
171
  typer.secho("PyroLab daemon launched.", fg=typer.colors.GREEN)
124
172
  else:
125
173
  typer.secho("PyroLab daemon is already running!", fg=typer.colors.RED)
126
174
  raise typer.Abort()
127
175
 
176
+
128
177
  @app.command()
129
178
  def down():
130
179
  """
@@ -136,6 +185,7 @@ def down():
136
185
  sleep(0.1)
137
186
  typer.secho("PyroLab daemon shutdown.", fg=typer.colors.GREEN)
138
187
 
188
+
139
189
  @app.command()
140
190
  def reload():
141
191
  """
@@ -150,6 +200,7 @@ def reload():
150
200
  else:
151
201
  typer.secho("PyroLab daemon reload failed.", fg=typer.colors.RED)
152
202
 
203
+
153
204
  @app.command()
154
205
  def ps():
155
206
  """
@@ -158,28 +209,38 @@ def ps():
158
209
  daemon = get_daemon()
159
210
  typer.echo(daemon.ps())
160
211
 
212
+
161
213
  ###############################################################################
162
214
  # pyrolab config
163
215
  ###############################################################################
164
216
 
165
217
  config_app = typer.Typer()
166
- app.add_typer(config_app, name="config", help="Configure PyroLab nameservers, daemons, and services.")
218
+ app.add_typer(
219
+ config_app,
220
+ name="config",
221
+ help="Configure PyroLab nameservers, daemons, and services.",
222
+ )
223
+
167
224
 
168
225
  @config_app.command("update")
169
226
  def config_update(filename: str):
170
227
  """Update the configuration file"""
171
228
  update_config(filename)
172
-
229
+
230
+
173
231
  @config_app.command("reset")
174
232
  def config_reset():
175
233
  """Reset the configuration file"""
176
- delete = typer.confirm("Are you sure you want to reset the configuration? This cannot be undone.")
234
+ delete = typer.confirm(
235
+ "Are you sure you want to reset the configuration? This cannot be undone."
236
+ )
177
237
  if not delete:
178
238
  typer.secho("No changes made.", fg=typer.colors.GREEN)
179
239
  raise typer.Exit()
180
240
  reset_config()
181
241
  typer.secho("Configuration reset.", fg=typer.colors.RED)
182
242
 
243
+
183
244
  @config_app.command("export")
184
245
  def config_export(filename: str):
185
246
  """Export the configuration file"""
@@ -189,12 +250,16 @@ def config_export(filename: str):
189
250
  typer.secho("No configuration file found.", fg=typer.colors.RED)
190
251
  raise typer.Abort()
191
252
 
253
+
192
254
  ###############################################################################
193
255
  # pyrolab start
194
256
  ###############################################################################
195
257
 
196
258
  start_app = typer.Typer()
197
- app.add_typer(start_app, name="start", help="Start a nameserver or daemon (and its services).")
259
+ app.add_typer(
260
+ start_app, name="start", help="Start a nameserver or daemon (and its services)."
261
+ )
262
+
198
263
 
199
264
  @start_app.command("nameserver")
200
265
  def start_nameserver(name: str):
@@ -204,6 +269,7 @@ def start_nameserver(name: str):
204
269
  daemon = get_daemon()
205
270
  daemon.start_nameserver(name)
206
271
 
272
+
207
273
  @start_app.command("daemon")
208
274
  def start_daemon(name: str):
209
275
  """
@@ -212,116 +278,110 @@ def start_daemon(name: str):
212
278
  daemon = get_daemon()
213
279
  daemon.start_daemon(name)
214
280
 
281
+
215
282
  ###############################################################################
216
283
  # pyrolab stop
217
284
  ###############################################################################
218
285
 
219
286
  stop_app = typer.Typer()
220
- app.add_typer(stop_app, name="stop", help="Stop a nameserver or daemon (and its services).")
287
+ app.add_typer(
288
+ stop_app, name="stop", help="Stop a nameserver or daemon (and its services)."
289
+ )
290
+
221
291
 
222
292
  @stop_app.command("nameserver")
223
- def stop_nameserver(name: Optional[str] = typer.Argument(None, help="Name of the service to stop"),):
293
+ def stop_nameserver(
294
+ name: Optional[str] = typer.Argument(None, help="Name of the service to stop"),
295
+ ):
224
296
  """
225
297
  Stop a nameserver.
226
298
  """
227
299
  daemon = get_daemon()
228
300
  daemon.stop_nameserver(name)
229
301
 
302
+
230
303
  @stop_app.command("daemon")
231
- def stop_daemon(name: Optional[str] = typer.Argument(None, help="Name of the service to stop"),):
304
+ def stop_daemon(
305
+ name: Optional[str] = typer.Argument(None, help="Name of the service to stop"),
306
+ ):
232
307
  """
233
308
  Stop a daemon.
234
309
  """
235
310
  daemon = get_daemon()
236
311
  daemon.stop_daemon(name)
237
312
 
313
+
238
314
  ###############################################################################
239
315
  # pyrolab info
240
316
  ###############################################################################
241
317
 
242
- info_app = typer.Typer()
243
- app.add_typer(info_app, name="info", help="Print details about a nameserver, daemon, or service.")
244
318
 
245
- @info_app.command("nameserver")
246
- def info_nameserver(
247
- name: str,
248
- ):
249
- """
250
- Get information on a nameserver.
251
- """
252
- if RUNTIME_CONFIG.exists():
253
- config = PyroLabConfiguration.from_file(RUNTIME_CONFIG)
254
- elif USER_CONFIG_FILE.exists():
255
- config = PyroLabConfiguration.from_file(USER_CONFIG_FILE)
256
- else:
257
- typer.secho("No configuration file found.", fg=typer.colors.RED)
258
- raise typer.Exit()
259
-
260
- if name in config.nameservers:
261
- info = textwrap.indent(str(config.nameservers[name].yaml()), ' ')
262
- typer.echo(f"{name}\n{info}")
263
- else:
264
- typer.secho("Nameserver not found.", fg=typer.colors.RED)
265
- raise typer.Exit()
266
-
267
- @info_app.command("daemon")
268
- def info_daemon(
269
- name: str,
270
- ):
319
+ @app.command()
320
+ def info():
271
321
  """
272
- Get information on a daemon.
322
+ Show details about the current PyroLab configuration.
273
323
  """
274
324
  if RUNTIME_CONFIG.exists():
275
325
  config = PyroLabConfiguration.from_file(RUNTIME_CONFIG)
276
326
  elif USER_CONFIG_FILE.exists():
277
327
  config = PyroLabConfiguration.from_file(USER_CONFIG_FILE)
278
328
  else:
279
- typer.secho("No configuration file found.", fg=typer.colors.RED)
329
+ typer.secho("No configuration installed.", fg=typer.colors.RED)
280
330
  raise typer.Exit()
281
331
 
282
- if name in config.daemons:
283
- info = textwrap.indent(str(config.daemons[name].yaml()), ' ')
284
- typer.echo(f"{name}\n{info}")
285
- else:
286
- typer.secho("Daemon not found.", fg=typer.colors.RED)
287
- raise typer.Exit()
332
+ ns_data = []
333
+ for name in config.nameservers:
334
+ ns_data.append({"name": name, **config.nameservers[name].dict()})
335
+ for item in ns_data:
336
+ item["ns_autoclean"] = (
337
+ f'{item["ns_autoclean"]} sec' if item["ns_autoclean"] else "Off"
338
+ )
339
+ if ns_data:
340
+ typer.echo("\nNameservers")
341
+ typer.echo(tabulate(ns_data, headers="keys", tablefmt="rounded_grid"))
342
+
343
+ daemon_data = []
344
+ for name in config.daemons:
345
+ daemon_data.append({"name": name, **config.daemons[name].dict()})
346
+ for item in daemon_data:
347
+ item["nameservers"] = "".join([f"- {name}\n" for name in item["nameservers"]])
348
+ if daemon_data:
349
+ typer.echo("\nDaemons")
350
+ typer.echo(tabulate(daemon_data, headers="keys", tablefmt="rounded_grid"))
351
+
352
+ service_data = []
353
+ for name in config.services:
354
+ service_data.append({"name": name, **config.services[name].dict()})
355
+ for item in service_data:
356
+ item["parameters"] = "".join(
357
+ [f"{k}: {v}\n" for k, v in item["parameters"].items()]
358
+ )
359
+ item["nameservers"] = "".join([f"- {name}\n" for name in item["nameservers"]])
360
+ if service_data:
361
+ typer.echo("\nServices")
362
+ typer.echo(tabulate(service_data, headers="keys", tablefmt="rounded_grid"))
288
363
 
289
- @info_app.command("service")
290
- def info_service(
291
- name: str,
292
- ):
293
- """
294
- Get information on a service.
295
- """
296
- if RUNTIME_CONFIG.exists():
297
- config = PyroLabConfiguration.from_file(RUNTIME_CONFIG)
298
- elif USER_CONFIG_FILE.exists():
299
- config = PyroLabConfiguration.from_file(USER_CONFIG_FILE)
300
- else:
301
- typer.secho("No configuration file found.", fg=typer.colors.RED)
302
- raise typer.Exit()
303
-
304
- if name in config.services:
305
- info = textwrap.indent(str(config.services[name].yaml()), ' ')
306
- typer.echo(f"{name}\n{info}")
307
- else:
308
- typer.secho("Service not found.", fg=typer.colors.RED)
309
- typer.echo(config.services.keys())
310
- raise typer.Exit()
311
364
 
312
365
  ###############################################################################
313
366
  # pyrolab logs
314
367
  ###############################################################################
315
368
 
316
369
  logs_app = typer.Typer()
317
- app.add_typer(logs_app, name="logs")
370
+ app.add_typer(logs_app, name="logs", help="Compile and export log files.")
371
+
318
372
 
319
373
  @logs_app.command("clean")
320
374
  def logs_clean():
321
375
  """
322
376
  Deletes all log files.
323
377
  """
324
- [f.unlink() for f in PYROLAB_LOGDIR.glob("*.*")]
378
+ for f in PYROLAB_LOGDIR.glob("*.*"):
379
+ # Try to delete file if not in use
380
+ try:
381
+ f.unlink()
382
+ except PermissionError:
383
+ pass
384
+
325
385
 
326
386
  def try_itr(func: Callable, itr: Iterable, *exceptions, **kwargs):
327
387
  """
@@ -334,21 +394,45 @@ def try_itr(func: Callable, itr: Iterable, *exceptions, **kwargs):
334
394
  except exceptions:
335
395
  pass
336
396
 
397
+
337
398
  @logs_app.command("export")
338
399
  def logs_export(filename: str):
339
400
  """
340
401
  Exports the log file to a file.
341
402
  """
342
- f_names = PYROLAB_LOGDIR.glob('*.*')
403
+ f_names = PYROLAB_LOGDIR.glob("*.*")
343
404
  lines = list(fileinput.input(f_names))
344
- t_fmt = "%Y-%m-%d %H:%M:%S.%f" # format of time stamps
345
- t_pat = re.compile(r'\[(.+?)\]') # pattern to extract timestamp
346
- lines = list(try_itr(lambda l: strptime(t_pat.search(l).group(1), t_fmt), lines, AttributeError))
347
- with Path(filename).open(mode='w') as f:
348
- for l in sorted(lines, key=lambda l: strptime(t_pat.search(l).group(1), t_fmt)):
349
- f.write(l)
405
+ t_fmt = "%Y-%m-%d %H:%M:%S.%f" # format of time stamps
406
+ t_pat = re.compile(r"\[(.+?)\]") # pattern to extract timestamp
407
+
408
+ # Group lines into log entries
409
+ log_entries = []
410
+ current_entry = []
411
+ for line in lines:
412
+ # If we've found a new timestamp and we have a current entry, add it to
413
+ # the list and start a new entry.
414
+ if t_pat.match(line) and current_entry:
415
+ log_entries.append("".join(current_entry))
416
+ current_entry = [line]
417
+ # Otherwise, keep extending the current entry.
418
+ else:
419
+ current_entry.append(line)
420
+ # Add the last entry to the list
421
+ if current_entry:
422
+ log_entries.append("".join(current_entry))
423
+
424
+ # Sort log entries by timestamp
425
+ log_entries = sorted(
426
+ log_entries, key=lambda entry: strptime(t_pat.search(entry).group(1), t_fmt)
427
+ )
428
+
429
+ # Write sorted log entries to the output file
430
+ with Path(filename).open(mode="w") as f:
431
+ for entry in log_entries:
432
+ f.write(entry)
350
433
  typer.secho(f"Exported logs to {filename}", fg=typer.colors.GREEN)
351
434
 
435
+
352
436
  ###############################################################################
353
437
  # pyrolab rename
354
438
  ###############################################################################
@@ -356,10 +440,11 @@ def logs_export(filename: str):
356
440
  rename_app = typer.Typer()
357
441
  app.add_typer(rename_app, name="rename", help="Rename a nameserver, daemon or service.")
358
442
 
443
+
359
444
  @rename_app.command("nameserver")
360
445
  def rename_nameserver(
361
- old_name: str,
362
- new_name: str,
446
+ old_name: str,
447
+ new_name: str,
363
448
  ):
364
449
  """
365
450
  Rename a nameserver.
@@ -376,10 +461,11 @@ def rename_nameserver(
376
461
  typer.secho("No user configuration file found.", fg=typer.colors.RED)
377
462
  raise typer.Exit()
378
463
 
464
+
379
465
  @rename_app.command("daemon")
380
466
  def rename_daemon(
381
- old_name: str,
382
- new_name: str,
467
+ old_name: str,
468
+ new_name: str,
383
469
  ):
384
470
  """
385
471
  Rename a daemon.
@@ -396,10 +482,11 @@ def rename_daemon(
396
482
  typer.secho("No user configuration file found.", fg=typer.colors.RED)
397
483
  raise typer.Exit()
398
484
 
485
+
399
486
  @rename_app.command("service")
400
487
  def rename_service(
401
- old_name: str,
402
- new_name: str,
488
+ old_name: str,
489
+ new_name: str,
403
490
  ):
404
491
  """
405
492
  Rename a service.
@@ -416,6 +503,41 @@ def rename_service(
416
503
  typer.secho("No user configuration file found.", fg=typer.colors.RED)
417
504
  raise typer.Exit()
418
505
 
506
+
507
+ ###############################################################################
508
+ # pyrolab ns_list
509
+ ###############################################################################
510
+
511
+
512
+ @app.command("nslist")
513
+ def ns_list(
514
+ host: str = typer.Option(
515
+ "localhost",
516
+ "--host",
517
+ "-h",
518
+ help="Nameserver host to list all registered services for (default localhost).",
519
+ ),
520
+ port: int = typer.Option(
521
+ 9090, "--port", "-p", help="Port to use for the PyroLab daemon (default 9090)."
522
+ ),
523
+ ):
524
+ """
525
+ List all services registered with a nameserver.
526
+ """
527
+ from pyrolab.api import locate_ns
528
+
529
+ ns = locate_ns(host=host, port=port)
530
+ services = ns.list(return_metadata=True)
531
+
532
+ listing = []
533
+ for k, v in services.items():
534
+ name = k
535
+ uri, description = v
536
+ listing.append([name, uri, ": ".join(description)])
537
+
538
+ typer.echo(tabulate(listing, headers=["NAME", "URI", "DESCRIPTION"]))
539
+
540
+
419
541
  ###############################################################################
420
542
  # pyrolab restart
421
543
  ###############################################################################