meerschaum 2.2.6__py3-none-any.whl → 2.3.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 (80) hide show
  1. meerschaum/__init__.py +6 -1
  2. meerschaum/__main__.py +9 -9
  3. meerschaum/_internal/arguments/__init__.py +1 -1
  4. meerschaum/_internal/arguments/_parse_arguments.py +72 -6
  5. meerschaum/_internal/arguments/_parser.py +45 -15
  6. meerschaum/_internal/docs/index.py +265 -8
  7. meerschaum/_internal/entry.py +167 -37
  8. meerschaum/_internal/shell/Shell.py +290 -99
  9. meerschaum/_internal/shell/updates.py +175 -0
  10. meerschaum/actions/__init__.py +29 -17
  11. meerschaum/actions/api.py +12 -12
  12. meerschaum/actions/attach.py +113 -0
  13. meerschaum/actions/copy.py +68 -41
  14. meerschaum/actions/delete.py +112 -50
  15. meerschaum/actions/edit.py +3 -3
  16. meerschaum/actions/install.py +40 -32
  17. meerschaum/actions/pause.py +44 -27
  18. meerschaum/actions/register.py +19 -5
  19. meerschaum/actions/restart.py +107 -0
  20. meerschaum/actions/show.py +130 -159
  21. meerschaum/actions/start.py +161 -100
  22. meerschaum/actions/stop.py +78 -42
  23. meerschaum/actions/sync.py +3 -3
  24. meerschaum/actions/upgrade.py +28 -36
  25. meerschaum/api/_events.py +25 -1
  26. meerschaum/api/_oauth2.py +2 -0
  27. meerschaum/api/_websockets.py +2 -2
  28. meerschaum/api/dash/callbacks/jobs.py +36 -44
  29. meerschaum/api/dash/jobs.py +89 -78
  30. meerschaum/api/routes/__init__.py +1 -0
  31. meerschaum/api/routes/_actions.py +148 -17
  32. meerschaum/api/routes/_jobs.py +407 -0
  33. meerschaum/api/routes/_pipes.py +25 -25
  34. meerschaum/config/_default.py +1 -0
  35. meerschaum/config/_formatting.py +1 -0
  36. meerschaum/config/_jobs.py +1 -1
  37. meerschaum/config/_paths.py +11 -0
  38. meerschaum/config/_shell.py +84 -67
  39. meerschaum/config/_version.py +1 -1
  40. meerschaum/config/static/__init__.py +18 -0
  41. meerschaum/connectors/Connector.py +13 -7
  42. meerschaum/connectors/__init__.py +28 -15
  43. meerschaum/connectors/api/APIConnector.py +27 -1
  44. meerschaum/connectors/api/_actions.py +71 -6
  45. meerschaum/connectors/api/_jobs.py +368 -0
  46. meerschaum/connectors/api/_misc.py +1 -1
  47. meerschaum/connectors/api/_pipes.py +85 -84
  48. meerschaum/connectors/api/_request.py +13 -9
  49. meerschaum/connectors/parse.py +27 -15
  50. meerschaum/core/Pipe/_bootstrap.py +16 -8
  51. meerschaum/core/Pipe/_sync.py +3 -0
  52. meerschaum/jobs/_Executor.py +69 -0
  53. meerschaum/jobs/_Job.py +899 -0
  54. meerschaum/jobs/__init__.py +396 -0
  55. meerschaum/jobs/systemd.py +694 -0
  56. meerschaum/plugins/__init__.py +97 -12
  57. meerschaum/utils/daemon/Daemon.py +352 -147
  58. meerschaum/utils/daemon/FileDescriptorInterceptor.py +19 -10
  59. meerschaum/utils/daemon/RotatingFile.py +22 -8
  60. meerschaum/utils/daemon/StdinFile.py +121 -0
  61. meerschaum/utils/daemon/__init__.py +42 -27
  62. meerschaum/utils/daemon/_names.py +15 -13
  63. meerschaum/utils/formatting/__init__.py +83 -37
  64. meerschaum/utils/formatting/_jobs.py +146 -55
  65. meerschaum/utils/formatting/_shell.py +6 -0
  66. meerschaum/utils/misc.py +41 -22
  67. meerschaum/utils/packages/__init__.py +21 -15
  68. meerschaum/utils/packages/_packages.py +9 -6
  69. meerschaum/utils/process.py +9 -9
  70. meerschaum/utils/prompt.py +20 -7
  71. meerschaum/utils/schedule.py +21 -15
  72. meerschaum/utils/venv/__init__.py +2 -2
  73. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/METADATA +22 -25
  74. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/RECORD +80 -70
  75. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/WHEEL +1 -1
  76. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/LICENSE +0 -0
  77. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/NOTICE +0 -0
  78. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/entry_points.txt +0 -0
  79. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/top_level.txt +0 -0
  80. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/zip-safe +0 -0
@@ -9,10 +9,12 @@ Intercept OS-level file descriptors.
9
9
  import os
10
10
  import select
11
11
  import traceback
12
+ import errno
12
13
  from threading import Event
13
14
  from datetime import datetime
14
15
  from meerschaum.utils.typing import Callable
15
16
  from meerschaum.utils.warnings import warn
17
+ from meerschaum.config.paths import DAEMON_ERROR_LOG_PATH
16
18
 
17
19
  FD_CLOSED: int = 9
18
20
  STOP_READING_FD_EVENT: Event = Event()
@@ -65,8 +67,13 @@ class FileDescriptorInterceptor:
65
67
  except BlockingIOError:
66
68
  continue
67
69
  except OSError as e:
68
- from meerschaum.utils.warnings import warn
69
- warn(f"OSError in FileDescriptorInterceptor: {e}")
70
+ if e.errno == errno.EBADF:
71
+ ### File descriptor is closed.
72
+ pass
73
+ elif e.errno == errno.EINTR:
74
+ continue # Interrupted system call, just try again
75
+ else:
76
+ warn(f"OSError in FileDescriptorInterceptor: {e}")
70
77
  break
71
78
 
72
79
  try:
@@ -86,9 +93,11 @@ class FileDescriptorInterceptor:
86
93
  else data.replace(b'\n', b'\n' + injected_bytes)
87
94
  )
88
95
  os.write(self.new_file_descriptor, modified_data)
89
- except Exception as e:
90
- from meerschaum.utils.warnings import warn
91
- warn(f"Error in FileDescriptorInterceptor data processing: {e}")
96
+ except (BrokenPipeError, OSError):
97
+ break
98
+ except Exception:
99
+ with open(DAEMON_ERROR_LOG_PATH, 'a+', encoding='utf-8') as f:
100
+ f.write(traceback.format_exc())
92
101
  break
93
102
 
94
103
 
@@ -103,7 +112,7 @@ class FileDescriptorInterceptor:
103
112
  except OSError as e:
104
113
  if e.errno != FD_CLOSED:
105
114
  warn(
106
- f"Error while trying to close the duplicated file descriptor:\n"
115
+ "Error while trying to close the duplicated file descriptor:\n"
107
116
  + f"{traceback.format_exc()}"
108
117
  )
109
118
 
@@ -112,7 +121,7 @@ class FileDescriptorInterceptor:
112
121
  except OSError as e:
113
122
  if e.errno != FD_CLOSED:
114
123
  warn(
115
- f"Error while trying to close the write-pipe "
124
+ "Error while trying to close the write-pipe "
116
125
  + "to the intercepted file descriptor:\n"
117
126
  + f"{traceback.format_exc()}"
118
127
  )
@@ -121,7 +130,7 @@ class FileDescriptorInterceptor:
121
130
  except OSError as e:
122
131
  if e.errno != FD_CLOSED:
123
132
  warn(
124
- f"Error while trying to close the read-pipe "
133
+ "Error while trying to close the read-pipe "
125
134
  + "to the intercepted file descriptor:\n"
126
135
  + f"{traceback.format_exc()}"
127
136
  )
@@ -131,7 +140,7 @@ class FileDescriptorInterceptor:
131
140
  except OSError as e:
132
141
  if e.errno != FD_CLOSED:
133
142
  warn(
134
- f"Error while trying to close the signal-read-pipe "
143
+ "Error while trying to close the signal-read-pipe "
135
144
  + "to the intercepted file descriptor:\n"
136
145
  + f"{traceback.format_exc()}"
137
146
  )
@@ -141,7 +150,7 @@ class FileDescriptorInterceptor:
141
150
  except OSError as e:
142
151
  if e.errno != FD_CLOSED:
143
152
  warn(
144
- f"Error while trying to close the signal-write-pipe "
153
+ "Error while trying to close the signal-write-pipe "
145
154
  + "to the intercepted file descriptor:\n"
146
155
  + f"{traceback.format_exc()}"
147
156
  )
@@ -38,7 +38,7 @@ class RotatingFile(io.IOBase):
38
38
  max_file_size: Optional[int] = None,
39
39
  redirect_streams: bool = False,
40
40
  write_timestamps: bool = False,
41
- timestamp_format: str = '%Y-%m-%d %H:%M',
41
+ timestamp_format: Optional[str] = None,
42
42
  ):
43
43
  """
44
44
  Create a file-like object which manages other files.
@@ -63,12 +63,18 @@ class RotatingFile(io.IOBase):
63
63
 
64
64
  write_timestamps: bool, default False
65
65
  If `True`, prepend the current UTC timestamp to each line of the file.
66
+
67
+ timestamp_format: str, default None
68
+ If `write_timestamps` is `True`, use this format for the timestamps.
69
+ Defaults to `'%Y-%m-%d %H:%M'`.
66
70
  """
67
71
  self.file_path = pathlib.Path(file_path)
68
72
  if num_files_to_keep is None:
69
73
  num_files_to_keep = get_config('jobs', 'logs', 'num_files_to_keep')
70
74
  if max_file_size is None:
71
75
  max_file_size = get_config('jobs', 'logs', 'max_file_size')
76
+ if timestamp_format is None:
77
+ timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
72
78
  if num_files_to_keep < 2:
73
79
  raise ValueError("At least 2 files must be kept.")
74
80
  if max_file_size < 1:
@@ -232,10 +238,10 @@ class RotatingFile(io.IOBase):
232
238
 
233
239
 
234
240
  def refresh_files(
235
- self,
236
- potential_new_len: int = 0,
237
- start_interception: bool = False,
238
- ) -> '_io.TextUIWrapper':
241
+ self,
242
+ potential_new_len: int = 0,
243
+ start_interception: bool = False,
244
+ ) -> '_io.TextUIWrapper':
239
245
  """
240
246
  Check the state of the subfiles.
241
247
  If the latest subfile is too large, create a new file and delete old ones.
@@ -339,7 +345,7 @@ class RotatingFile(io.IOBase):
339
345
 
340
346
  def get_timestamp_prefix_str(self) -> str:
341
347
  """
342
- Return the current minute prefixm string.
348
+ Return the current minute prefix string.
343
349
  """
344
350
  return datetime.now(timezone.utc).strftime(self.timestamp_format) + ' | '
345
351
 
@@ -371,6 +377,9 @@ class RotatingFile(io.IOBase):
371
377
  self._current_file_obj.write(data)
372
378
  if suffix_str:
373
379
  self._current_file_obj.write(suffix_str)
380
+ except BrokenPipeError:
381
+ warn("BrokenPipeError encountered. The daemon may have been terminated.")
382
+ return
374
383
  except Exception as e:
375
384
  warn(f"Failed to write to subfile:\n{traceback.format_exc()}")
376
385
  self.flush()
@@ -565,7 +574,8 @@ class RotatingFile(io.IOBase):
565
574
  return
566
575
 
567
576
  self._cursor = (max_ix, position)
568
- self._current_file_obj.seek(position)
577
+ if self._current_file_obj is not None:
578
+ self._current_file_obj.seek(position)
569
579
 
570
580
 
571
581
  def flush(self) -> None:
@@ -581,10 +591,14 @@ class RotatingFile(io.IOBase):
581
591
  if self.redirect_streams:
582
592
  try:
583
593
  sys.stdout.flush()
594
+ except BrokenPipeError:
595
+ pass
584
596
  except Exception as e:
585
597
  warn(f"Failed to flush STDOUT:\n{traceback.format_exc()}")
586
598
  try:
587
599
  sys.stderr.flush()
600
+ except BrokenPipeError:
601
+ pass
588
602
  except Exception as e:
589
603
  warn(f"Failed to flush STDERR:\n{traceback.format_exc()}")
590
604
 
@@ -596,7 +610,6 @@ class RotatingFile(io.IOBase):
596
610
  if not self.write_timestamps:
597
611
  return
598
612
 
599
- threads = self.__dict__.get('_interceptor_threads', [])
600
613
  self._stdout_interceptor = FileDescriptorInterceptor(
601
614
  sys.stdout.fileno(),
602
615
  self.get_timestamp_prefix_str,
@@ -639,6 +652,7 @@ class RotatingFile(io.IOBase):
639
652
  """
640
653
  if not self.write_timestamps:
641
654
  return
655
+
642
656
  interceptors = self.__dict__.get('_interceptors', [])
643
657
  interceptor_threads = self.__dict__.get('_interceptor_threads', [])
644
658
 
@@ -0,0 +1,121 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Create a file manager to pass STDIN to the Daemon.
7
+ """
8
+
9
+ import io
10
+ import pathlib
11
+ import time
12
+ import os
13
+ import selectors
14
+ import traceback
15
+
16
+ from meerschaum.utils.typing import Optional, Union
17
+ from meerschaum.utils.warnings import warn
18
+
19
+
20
+ class StdinFile(io.TextIOBase):
21
+ """
22
+ Redirect user input into a Daemon's context.
23
+ """
24
+ def __init__(
25
+ self,
26
+ file_path: Union[pathlib.Path, str],
27
+ lock_file_path: Optional[pathlib.Path] = None,
28
+ ):
29
+ if isinstance(file_path, str):
30
+ file_path = pathlib.Path(file_path)
31
+
32
+ self.file_path = file_path
33
+ self.blocking_file_path = (
34
+ lock_file_path
35
+ if lock_file_path is not None
36
+ else (file_path.parent / (file_path.name + '.block'))
37
+ )
38
+ self._file_handler = None
39
+ self._fd = None
40
+ self.sel = selectors.DefaultSelector()
41
+
42
+ @property
43
+ def file_handler(self):
44
+ """
45
+ Return the read file handler to the provided file path.
46
+ """
47
+ if self._file_handler is not None:
48
+ return self._file_handler
49
+
50
+ if self.file_path.exists():
51
+ self.file_path.unlink()
52
+
53
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
54
+ os.mkfifo(self.file_path.as_posix(), mode=0o600)
55
+
56
+ self._fd = os.open(self.file_path, os.O_RDONLY | os.O_NONBLOCK)
57
+ self._file_handler = os.fdopen(self._fd, 'rb', buffering=0)
58
+ self.sel.register(self._file_handler, selectors.EVENT_READ)
59
+ return self._file_handler
60
+
61
+ def write(self, data):
62
+ if isinstance(data, str):
63
+ data = data.encode('utf-8')
64
+
65
+ with open(self.file_path, 'wb') as f:
66
+ f.write(data)
67
+
68
+ def fileno(self):
69
+ fileno = self.file_handler.fileno()
70
+ return fileno
71
+
72
+ def read(self, size=-1):
73
+ """
74
+ Read from the FIFO pipe, blocking on EOFError.
75
+ """
76
+ _ = self.file_handler
77
+ while True:
78
+ try:
79
+ data = self._file_handler.read(size)
80
+ if data:
81
+ try:
82
+ if self.blocking_file_path.exists():
83
+ self.blocking_file_path.unlink()
84
+ except Exception:
85
+ warn(traceback.format_exc())
86
+ return data.decode('utf-8')
87
+ except (OSError, EOFError):
88
+ pass
89
+
90
+ self.blocking_file_path.touch()
91
+ time.sleep(0.1)
92
+
93
+ def readline(self, size=-1):
94
+ line = ''
95
+ while True:
96
+ data = self.read(1)
97
+ if not data or data == '\n':
98
+ break
99
+ line += data
100
+
101
+ return line
102
+
103
+ def close(self):
104
+ if self._file_handler is not None:
105
+ self.sel.unregister(self._file_handler)
106
+ self._file_handler.close()
107
+ os.close(self._fd)
108
+ self._file_handler = None
109
+ self._fd = None
110
+
111
+ super().close()
112
+
113
+ def is_open(self):
114
+ return self._file_handler is not None
115
+
116
+
117
+ def __str__(self) -> str:
118
+ return f"StdinFile('{self.file_path}')"
119
+
120
+ def __repr__(self) -> str:
121
+ return str(self)
@@ -10,9 +10,11 @@ from __future__ import annotations
10
10
  import os, pathlib, shutil, json, datetime, threading, shlex
11
11
  from meerschaum.utils.typing import SuccessTuple, List, Optional, Callable, Any, Dict
12
12
  from meerschaum.config._paths import DAEMON_RESOURCES_PATH
13
+ from meerschaum.utils.daemon.StdinFile import StdinFile
13
14
  from meerschaum.utils.daemon.Daemon import Daemon
14
15
  from meerschaum.utils.daemon.RotatingFile import RotatingFile
15
16
  from meerschaum.utils.daemon.FileDescriptorInterceptor import FileDescriptorInterceptor
17
+ from meerschaum.utils.daemon._names import get_new_daemon_name
16
18
 
17
19
 
18
20
  def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
@@ -67,19 +69,17 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
67
69
  if daemon.status == 'running':
68
70
  return True, f"Daemon '{daemon}' is already running."
69
71
  return daemon.run(
70
- debug = debug,
71
- allow_dirty_run = True,
72
+ debug=debug,
73
+ allow_dirty_run=True,
72
74
  )
73
75
 
74
76
  success_tuple = run_daemon(
75
77
  entry,
76
78
  filtered_sysargs,
77
- daemon_id = _args.get('name', None) if _args else None,
78
- label = label,
79
- keep_daemon_output = ('--rm' not in sysargs)
79
+ daemon_id=_args.get('name', None) if _args else None,
80
+ label=label,
81
+ keep_daemon_output=('--rm' not in (sysargs or [])),
80
82
  )
81
- if not isinstance(success_tuple, tuple):
82
- success_tuple = False, str(success_tuple)
83
83
  return success_tuple
84
84
 
85
85
 
@@ -109,25 +109,25 @@ def daemon_action(**kw) -> SuccessTuple:
109
109
 
110
110
 
111
111
  def run_daemon(
112
- func: Callable[[Any], Any],
113
- *args,
114
- daemon_id: Optional[str] = None,
115
- keep_daemon_output: bool = False,
116
- allow_dirty_run: bool = False,
117
- label: Optional[str] = None,
118
- **kw
119
- ) -> Any:
112
+ func: Callable[[Any], Any],
113
+ *args,
114
+ daemon_id: Optional[str] = None,
115
+ keep_daemon_output: bool = True,
116
+ allow_dirty_run: bool = False,
117
+ label: Optional[str] = None,
118
+ **kw
119
+ ) -> Any:
120
120
  """Execute a function as a daemon."""
121
121
  daemon = Daemon(
122
122
  func,
123
- daemon_id = daemon_id,
124
- target_args = [arg for arg in args],
125
- target_kw = kw,
126
- label = label,
123
+ daemon_id=daemon_id,
124
+ target_args=[arg for arg in args],
125
+ target_kw=kw,
126
+ label=label,
127
127
  )
128
128
  return daemon.run(
129
- keep_daemon_output = keep_daemon_output,
130
- allow_dirty_run = allow_dirty_run,
129
+ keep_daemon_output=keep_daemon_output,
130
+ allow_dirty_run=allow_dirty_run,
131
131
  )
132
132
 
133
133
 
@@ -183,7 +183,11 @@ def get_daemon_ids() -> List[str]:
183
183
  """
184
184
  Return the IDs of all daemons on disk.
185
185
  """
186
- return sorted(os.listdir(DAEMON_RESOURCES_PATH))
186
+ return [
187
+ daemon_dir
188
+ for daemon_dir in sorted(os.listdir(DAEMON_RESOURCES_PATH))
189
+ if (DAEMON_RESOURCES_PATH / daemon_dir / 'properties.json').exists()
190
+ ]
187
191
 
188
192
 
189
193
  def get_running_daemons(daemons: Optional[List[Daemon]] = None) -> List[Daemon]:
@@ -227,10 +231,11 @@ def get_stopped_daemons(daemons: Optional[List[Daemon]] = None) -> List[Daemon]:
227
231
 
228
232
 
229
233
  def get_filtered_daemons(
230
- filter_list: Optional[List[str]] = None,
231
- warn: bool = False,
232
- ) -> List[Daemon]:
233
- """Return a list of `Daemons` filtered by a list of `daemon_ids`.
234
+ filter_list: Optional[List[str]] = None,
235
+ warn: bool = False,
236
+ ) -> List[Daemon]:
237
+ """
238
+ Return a list of `Daemons` filtered by a list of `daemon_ids`.
234
239
  Only `Daemons` that exist are returned.
235
240
 
236
241
  If `filter_list` is `None` or empty, return all `Daemons` (from `get_daemons()`).
@@ -252,13 +257,14 @@ def get_filtered_daemons(
252
257
  if not filter_list:
253
258
  daemons = get_daemons()
254
259
  return [d for d in daemons if not d.hidden]
260
+
255
261
  from meerschaum.utils.warnings import warn as _warn
256
262
  daemons = []
257
263
  for d_id in filter_list:
258
264
  try:
259
265
  d = Daemon(daemon_id=d_id)
260
266
  _exists = d.path.exists()
261
- except Exception as e:
267
+ except Exception:
262
268
  _exists = False
263
269
  if not _exists:
264
270
  if warn:
@@ -268,3 +274,12 @@ def get_filtered_daemons(
268
274
  pass
269
275
  daemons.append(d)
270
276
  return daemons
277
+
278
+
279
+ def running_in_daemon() -> bool:
280
+ """
281
+ Return whether the current thread is running in a Daemon context.
282
+ """
283
+ from meerschaum.config.static import STATIC_CONFIG
284
+ daemon_env_var = STATIC_CONFIG['environment']['daemon_id']
285
+ return daemon_env_var in os.environ
@@ -18,7 +18,8 @@ _bank: Dict[str, Dict[str, List[str]]] = {
18
18
  'bright', 'dark', 'neon',
19
19
  ],
20
20
  'sizes': [
21
- 'big', 'small', 'large', 'huge', 'tiny', 'long', 'short', 'grand', 'mini', 'micro'
21
+ 'big', 'small', 'large', 'huge', 'tiny', 'long', 'short', 'average', 'mini', 'micro',
22
+ 'maximum', 'minimum', 'median',
22
23
  ],
23
24
  'personalities': [
24
25
  'groovy', 'cool', 'awesome', 'nice', 'fantastic', 'sweet', 'great', 'amazing',
@@ -26,29 +27,36 @@ _bank: Dict[str, Dict[str, List[str]]] = {
26
27
  ],
27
28
  'emotions': [
28
29
  'angry', 'happy', 'excited', 'suspicious', 'sad', 'thankful', 'grateful', 'satisfied',
30
+ 'peaceful', 'ferocious', 'content',
29
31
  ],
30
32
  'sensations': [
31
33
  'sleepy', 'awake', 'alert', 'thirsty', 'comfy', 'warm', 'cold', 'chilly', 'soft',
32
- 'smooth', 'chunky',
34
+ 'smooth', 'chunky', 'hungry',
33
35
  ],
34
36
  'materials': [
35
37
  'golden', 'silver', 'metal', 'plastic', 'wool', 'wooden', 'nylon', 'fuzzy', 'silky',
38
+ 'suede', 'vinyl',
36
39
  ],
37
40
  'qualities': [
38
41
  'expensive', 'cheap', 'premier', 'best', 'favorite', 'better', 'good', 'affordable',
42
+ 'organic', 'electric',
39
43
  ],
40
44
  },
41
45
  'nouns' : {
42
46
  'animals': [
43
- 'mouse', 'fox', 'horse', 'dragon', 'pig', 'hippo', 'elephant' , 'tiger', 'deer',
47
+ 'mouse', 'fox', 'horse', 'pig', 'hippo', 'elephant' , 'tiger', 'deer', 'salmon',
44
48
  'gerbil', 'snake', 'turtle', 'rhino', 'dog', 'cat', 'giraffe', 'rabbit', 'squirrel',
45
49
  'unicorn', 'lizard', 'lion', 'bear', 'gazelle', 'whale', 'dolphin', 'fish', 'butterfly',
46
50
  'ladybug', 'fly', 'shrimp', 'flamingo', 'parrot', 'tuna', 'panda', 'lemur', 'duck',
47
51
  'seal', 'walrus', 'seagull', 'iguana', 'salamander', 'kitten', 'puppy', 'octopus',
48
52
  ],
53
+ 'weather': [
54
+ 'rain', 'sun', 'snow', 'wind', 'tornado', 'hurricane', 'blizzard', 'monsoon', 'storm',
55
+ 'shower', 'hail',
56
+ ],
49
57
  'plants': [
50
58
  'tree', 'flower', 'vine', 'fern', 'palm', 'palmetto', 'oak', 'pine', 'rose', 'lily',
51
- 'ivy',
59
+ 'ivy', 'leaf', 'shrubbery', 'acorn', 'fruit',
52
60
  ],
53
61
  'foods': [
54
62
  'pizza', 'sushi', 'apple', 'banana', 'sandwich', 'burger', 'taco', 'bratwurst',
@@ -74,8 +82,6 @@ _bank: Dict[str, Dict[str, List[str]]] = {
74
82
  },
75
83
  }
76
84
 
77
- _disallow_combinations: List[Tuple[str, str]] = []
78
-
79
85
  _adjectives: List[str]= []
80
86
  for category, items in _bank['adjectives'].items():
81
87
  _adjectives += items
@@ -84,7 +90,7 @@ _nouns: List[str] = []
84
90
  for category, items in _bank['nouns'].items():
85
91
  _nouns += items
86
92
 
87
- def generate_random_name(separator: str = '_'):
93
+ def generate_random_name(separator: str = '-'):
88
94
  """
89
95
  Return a random adjective and noun combination.
90
96
 
@@ -96,12 +102,8 @@ def generate_random_name(separator: str = '_'):
96
102
  -------
97
103
  A string containing an random adjective and random noun.
98
104
  """
99
- while True:
100
- adjective_category = random.choice(list(_bank['adjectives'].keys()))
101
- noun_category = random.choice(list(_bank['nouns'].keys()))
102
- if (adjective_category, noun_category) in _disallow_combinations:
103
- continue
104
- break
105
+ adjective_category = random.choice(list(_bank['adjectives'].keys()))
106
+ noun_category = random.choice(list(_bank['nouns'].keys()))
105
107
  return (
106
108
  random.choice(_bank['adjectives'][adjective_category])
107
109
  + separator