xulbux 1.6.5__py3-none-any.whl → 1.6.6__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.

Potentially problematic release.


This version of xulbux might be problematic. Click here for more details.

xulbux/xx_console.py CHANGED
@@ -1,31 +1,18 @@
1
1
  """
2
- Functions for logging and other small actions within the console:
3
- - `Console.get_args()`
4
- - `Console.user()`
5
- - `Console.is_admin()`
6
- - `Console.pause_exit()`
7
- - `Console.cls()`
8
- - `Console.log()`
9
- - `Console.debug()`
10
- - `Console.info()`
11
- - `Console.done()`
12
- - `Console.warn()`
13
- - `Console.fail()`
14
- - `Console.exit()`
15
- - `Console.confirm()`
16
- - `Console.restricted_input()`
17
- - `Console.pwd_input()`\n
18
- ------------------------------------------------------------------------------------------------------
2
+ Functions for logging and other small actions within the console.\n
3
+ ----------------------------------------------------------------------------------------------------------
19
4
  You can also use special formatting codes directly inside the log message to change their appearance.
20
- For more detailed information about formatting codes, see the the `xx_format_codes` description.
5
+ For more detailed information about formatting codes, see the the `xx_format_codes` module documentation.
21
6
  """
22
7
 
23
- from ._consts_ import DEFAULT, CHARS
24
- from .xx_format_codes import FormatCodes
8
+ from ._consts_ import COLOR, CHARS
9
+ from .xx_format_codes import FormatCodes, _COMPILED
25
10
  from .xx_string import String
26
- from .xx_color import *
11
+ from .xx_color import Color, rgba, hexa
27
12
 
28
- from contextlib import suppress
13
+ from prompt_toolkit.key_binding.key_bindings import KeyBindings
14
+ from typing import Optional
15
+ import prompt_toolkit as _prompt_toolkit
29
16
  import pyperclip as _pyperclip
30
17
  import keyboard as _keyboard
31
18
  import getpass as _getpass
@@ -35,8 +22,33 @@ import sys as _sys
35
22
  import os as _os
36
23
 
37
24
 
25
+ # YAPF: disable
26
+ class _ConsoleWidth:
27
+ def __get__(self, obj, owner=None):
28
+ return _os.get_terminal_size().columns
29
+
30
+ class _ConsoleHeight:
31
+ def __get__(self, obj, owner=None):
32
+ return _os.get_terminal_size().lines
33
+
34
+ class _ConsoleSize:
35
+ def __get__(self, obj, owner=None):
36
+ size = _os.get_terminal_size()
37
+ return (size.columns, size.lines)
38
+
39
+ class _ConsoleUser:
40
+ def __get__(self, obj, owner=None):
41
+ return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
42
+ # YAPF: enable
43
+
44
+
38
45
  class Console:
39
46
 
47
+ w: int = _ConsoleWidth()
48
+ h: int = _ConsoleHeight()
49
+ wh: tuple[int, int] = _ConsoleSize()
50
+ usr: str = _ConsoleUser()
51
+
40
52
  @staticmethod
41
53
  def get_args(find_args: dict) -> dict[str, dict[str, any]]:
42
54
  args = _sys.argv[1:]
@@ -54,18 +66,6 @@ class Console:
54
66
  results[arg_key] = {"exists": exists, "value": value}
55
67
  return results
56
68
 
57
- def w() -> int:
58
- return getattr(_shutil.get_terminal_size(), "columns", 80)
59
-
60
- def h() -> int:
61
- return getattr(_shutil.get_terminal_size(), "lines", 24)
62
-
63
- def wh() -> tuple[int, int]:
64
- return Console.w(), Console.h()
65
-
66
- def user() -> str:
67
- return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
68
-
69
69
  @staticmethod
70
70
  def pause_exit(
71
71
  pause: bool = False,
@@ -84,6 +84,7 @@ class Console:
84
84
  if exit:
85
85
  _sys.exit(exit_code)
86
86
 
87
+ @staticmethod
87
88
  def cls() -> None:
88
89
  """Will clear the console in addition to completely resetting the ANSI formats."""
89
90
  if _shutil.which("cls"):
@@ -94,33 +95,45 @@ class Console:
94
95
 
95
96
  @staticmethod
96
97
  def log(
97
- title: str,
98
+ title: Optional[str] = None,
98
99
  prompt: object = "",
100
+ format_linebreaks: bool = True,
99
101
  start: str = "",
100
102
  end: str = "\n",
101
103
  title_bg_color: hexa | rgba = None,
102
104
  default_color: hexa | rgba = None,
105
+ _console_tabsize: int = 8,
103
106
  ) -> None:
104
107
  """Will print a formatted log message:
105
108
  - `title` -⠀the title of the log message (e.g. `DEBUG`, `WARN`, `FAIL`, etc.)
106
109
  - `prompt` -⠀the log message
110
+ - `format_linebreaks` -⠀whether to format (indent after) the line breaks or not
107
111
  - `start` -⠀something to print before the log is printed
108
112
  - `end` -⠀something to print after the log is printed (e.g. `\\n`)
109
113
  - `title_bg_color` -⠀the background color of the `title`
110
114
  - `default_color` -⠀the default text color of the `prompt`\n
111
- --------------------------------------------------------------------------------
112
- The log message supports special formatting codes. For more detailed
113
- information about formatting codes, see `xx_format_codes` class description."""
115
+ -----------------------------------------------------------------------------------
116
+ The log message can be formatted with special formatting codes. For more detailed
117
+ information about formatting codes, see `xx_format_codes` module documentation."""
118
+ title = "" if title is None else title.strip().upper()
119
+ title_len, tab_len = len(title) + 4, _console_tabsize - ((len(title) + 4) % _console_tabsize)
114
120
  title_color = "_color" if not title_bg_color else Color.text_color_for_on_bg(title_bg_color)
115
- if title:
121
+ if format_linebreaks:
122
+ prompt_lst = (String.split_count(l, Console.w - (title_len + tab_len)) for l in str(prompt).splitlines())
123
+ prompt_lst = (item for lst in prompt_lst for item in (lst if isinstance(lst, list) else [lst]))
124
+ prompt = f"\n{' ' * title_len}\t".join(prompt_lst)
125
+ else:
126
+ prompt = str(prompt)
127
+ if title == "":
116
128
  FormatCodes.print(
117
- f'{start} [bold][{title_color}]{f"[BG:{title_bg_color}]" if title_bg_color else ""} {title.upper()}: [_]\t{f"[{default_color}]" if default_color else ""}{str(prompt)}[_]',
129
+ f'{start} {f"[{default_color}]" if default_color else ""}{str(prompt)}[_]',
118
130
  default_color=default_color,
119
131
  end=end,
120
132
  )
121
133
  else:
122
134
  FormatCodes.print(
123
- f'{start} {f"[{default_color}]" if default_color else ""}{str(prompt)}[_]',
135
+ f'{start} [bold][{title_color}]{f"[BG:{title_bg_color}]" if title_bg_color else ""} {title} [_]'
136
+ + f'\t{f"[{default_color}]" if default_color else ""}{prompt}[_]',
124
137
  default_color=default_color,
125
138
  end=end,
126
139
  )
@@ -129,94 +142,101 @@ class Console:
129
142
  def debug(
130
143
  prompt: object = "Point in program reached.",
131
144
  active: bool = True,
145
+ format_linebreaks: bool = True,
132
146
  start: str = "",
133
147
  end: str = "\n",
134
- title_bg_color: hexa | rgba = DEFAULT.color["yellow"],
135
- default_color: hexa | rgba = DEFAULT.text_color,
148
+ title_bg_color: hexa | rgba = COLOR.yellow,
149
+ default_color: hexa | rgba = COLOR.text,
136
150
  pause: bool = False,
137
151
  exit: bool = False,
138
152
  ) -> None:
139
153
  """A preset for `log()`: `DEBUG` log message with the options to pause
140
- at the message and exit the program after the message was printed."""
154
+ at the message and exit the program after the message was printed.
155
+ If `active` is false, no debug message will be printed."""
141
156
  if active:
142
- Console.log("DEBUG", prompt, start, end, title_bg_color, default_color)
157
+ Console.log("DEBUG", prompt, format_linebreaks, start, end, title_bg_color, default_color)
143
158
  Console.pause_exit(pause, exit)
144
159
 
145
160
  @staticmethod
146
161
  def info(
147
162
  prompt: object = "Program running.",
163
+ format_linebreaks: bool = True,
148
164
  start: str = "",
149
165
  end: str = "\n",
150
- title_bg_color: hexa | rgba = DEFAULT.color["blue"],
151
- default_color: hexa | rgba = DEFAULT.text_color,
166
+ title_bg_color: hexa | rgba = COLOR.blue,
167
+ default_color: hexa | rgba = COLOR.text,
152
168
  pause: bool = False,
153
169
  exit: bool = False,
154
170
  ) -> None:
155
171
  """A preset for `log()`: `INFO` log message with the options to pause
156
172
  at the message and exit the program after the message was printed."""
157
- Console.log("INFO", prompt, start, end, title_bg_color, default_color)
173
+ Console.log("INFO", prompt, format_linebreaks, start, end, title_bg_color, default_color)
158
174
  Console.pause_exit(pause, exit)
159
175
 
160
176
  @staticmethod
161
177
  def done(
162
178
  prompt: object = "Program finished.",
179
+ format_linebreaks: bool = True,
163
180
  start: str = "",
164
181
  end: str = "\n",
165
- title_bg_color: hexa | rgba = DEFAULT.color["teal"],
166
- default_color: hexa | rgba = DEFAULT.text_color,
182
+ title_bg_color: hexa | rgba = COLOR.teal,
183
+ default_color: hexa | rgba = COLOR.text,
167
184
  pause: bool = False,
168
185
  exit: bool = False,
169
186
  ) -> None:
170
187
  """A preset for `log()`: `DONE` log message with the options to pause
171
188
  at the message and exit the program after the message was printed."""
172
- Console.log("DONE", prompt, start, end, title_bg_color, default_color)
189
+ Console.log("DONE", prompt, format_linebreaks, start, end, title_bg_color, default_color)
173
190
  Console.pause_exit(pause, exit)
174
191
 
175
192
  @staticmethod
176
193
  def warn(
177
194
  prompt: object = "Important message.",
195
+ format_linebreaks: bool = True,
178
196
  start: str = "",
179
197
  end: str = "\n",
180
- title_bg_color: hexa | rgba = DEFAULT.color["orange"],
181
- default_color: hexa | rgba = DEFAULT.text_color,
198
+ title_bg_color: hexa | rgba = COLOR.orange,
199
+ default_color: hexa | rgba = COLOR.text,
182
200
  pause: bool = False,
183
201
  exit: bool = False,
184
202
  ) -> None:
185
203
  """A preset for `log()`: `WARN` log message with the options to pause
186
204
  at the message and exit the program after the message was printed."""
187
- Console.log("WARN", prompt, start, end, title_bg_color, default_color)
205
+ Console.log("WARN", prompt, format_linebreaks, start, end, title_bg_color, default_color)
188
206
  Console.pause_exit(pause, exit)
189
207
 
190
208
  @staticmethod
191
209
  def fail(
192
210
  prompt: object = "Program error.",
211
+ format_linebreaks: bool = True,
193
212
  start: str = "",
194
213
  end: str = "\n",
195
- title_bg_color: hexa | rgba = DEFAULT.color["red"],
196
- default_color: hexa | rgba = DEFAULT.text_color,
214
+ title_bg_color: hexa | rgba = COLOR.red,
215
+ default_color: hexa | rgba = COLOR.text,
197
216
  pause: bool = False,
198
217
  exit: bool = True,
199
218
  reset_ansi=True,
200
219
  ) -> None:
201
220
  """A preset for `log()`: `FAIL` log message with the options to pause
202
221
  at the message and exit the program after the message was printed."""
203
- Console.log("FAIL", prompt, start, end, title_bg_color, default_color)
222
+ Console.log("FAIL", prompt, format_linebreaks, start, end, title_bg_color, default_color)
204
223
  Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
205
224
 
206
225
  @staticmethod
207
226
  def exit(
208
227
  prompt: object = "Program ended.",
228
+ format_linebreaks: bool = True,
209
229
  start: str = "",
210
230
  end: str = "\n",
211
- title_bg_color: hexa | rgba = DEFAULT.color["magenta"],
212
- default_color: hexa | rgba = DEFAULT.text_color,
231
+ title_bg_color: hexa | rgba = COLOR.magenta,
232
+ default_color: hexa | rgba = COLOR.text,
213
233
  pause: bool = False,
214
234
  exit: bool = True,
215
235
  reset_ansi=True,
216
236
  ) -> None:
217
237
  """A preset for `log()`: `EXIT` log message with the options to pause
218
238
  at the message and exit the program after the message was printed."""
219
- Console.log("EXIT", prompt, start, end, title_bg_color, default_color)
239
+ Console.log("EXIT", prompt, format_linebreaks, start, end, title_bg_color, default_color)
220
240
  Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
221
241
 
222
242
  @staticmethod
@@ -226,28 +246,33 @@ class Console:
226
246
  end: str = "\n",
227
247
  box_bg_color: str | hexa | rgba = "green",
228
248
  default_color: hexa | rgba = "#000",
229
- _padding: int = 2,
249
+ w_padding: int = 2,
250
+ w_full: bool = False,
230
251
  ) -> None:
231
252
  """Will print a box, containing a formatted log message:
232
253
  - `*values` -⠀the box content (each value is on a new line)
233
254
  - `start` -⠀something to print before the log box is printed
234
255
  - `end` -⠀something to print after the log box is printed (e.g. `\\n`)
235
256
  - `box_bg_color` -⠀the box's background color
236
- - `default_color` -⠀the default text color of the `*values`\n
237
- --------------------------------------------------------------------------------
238
- The log message supports special formatting codes. For more detailed
239
- information about formatting codes, see `xx_format_codes` class description."""
240
- lines = [line for val in values for line in val.splitlines()]
257
+ - `default_color` -⠀the default text color of the `*values`
258
+ - `w_padding` -⠀the horizontal padding (in chars) to the box content
259
+ - `w_full` -⠀whether to make the box be the full console width or not\n
260
+ -----------------------------------------------------------------------------------
261
+ The box content can be formatted with special formatting codes. For more detailed
262
+ information about formatting codes, see `xx_format_codes` module documentation."""
263
+ lines = [line.strip() for val in values for line in val.splitlines()]
241
264
  unfmt_lines = [FormatCodes.remove_formatting(line) for line in lines]
242
265
  max_line_len = max(len(line) for line in unfmt_lines)
266
+ pad_w_full = (Console.w - (max_line_len + (2 * w_padding))) if w_full else 0
243
267
  lines = [
244
- f"[bg:{box_bg_color}]{' ' * _padding}{line}{' ' * (_padding + max_line_len - len(unfmt))}[_bg]"
245
- for line, unfmt in zip(lines, unfmt_lines)
268
+ f"[bg:{box_bg_color}]{' ' * w_padding}{line}" + " " *
269
+ ((w_padding + max_line_len - len(unfmt)) + pad_w_full) + "[_bg]" for line, unfmt in zip(lines, unfmt_lines)
246
270
  ]
271
+ pady = " " * (Console.w if w_full else max_line_len + (2 * w_padding))
247
272
  FormatCodes.print(
248
- f"{start}[bg:{box_bg_color}]{(pad := " " * (max_line_len + (2 * _padding)))}[_bg]\n"
249
- + "\n".join(lines)
250
- + f"\n[bg:{box_bg_color}]{pad}[_]",
273
+ f"{start}[bg:{box_bg_color}]{pady}[_bg]\n"
274
+ + _COMPILED["formatting"].sub(lambda m: f"{m.group(0)}[bg:{box_bg_color}]", "\n".join(lines))
275
+ + f"\n[bg:{box_bg_color}]{pady}[_bg]",
251
276
  default_color=default_color,
252
277
  sep="\n",
253
278
  end=end,
@@ -258,13 +283,13 @@ class Console:
258
283
  prompt: object = "Do you want to continue?",
259
284
  start="",
260
285
  end="\n",
261
- default_color: hexa | rgba = DEFAULT.color["cyan"],
286
+ default_color: hexa | rgba = COLOR.cyan,
262
287
  default_is_yes: bool = True,
263
288
  ) -> bool:
264
289
  """Ask a yes/no question.\n
265
- -------------------------------------------------------------------------------
266
- The question can be formatted with special formatting codes. For more detailed
267
- information about formatting codes, see the `xx_format_codes` description."""
290
+ ---------------------------------------------------------------------------------------
291
+ The prompt can be formatted with special formatting codes. For more detailed
292
+ information about formatting codes, see the `xx_format_codes` module documentation."""
268
293
  confirmed = input(
269
294
  FormatCodes.to_ansi(
270
295
  f'{start} {str(prompt)} [_|dim](({"Y" if default_is_yes else "y"}/{"n" if default_is_yes else "N"}): )',
@@ -280,21 +305,21 @@ class Console:
280
305
  prompt: object = "",
281
306
  start="",
282
307
  end="\n",
283
- default_color: hexa | rgba = DEFAULT.color["cyan"],
308
+ default_color: hexa | rgba = COLOR.cyan,
284
309
  allowed_chars: str = CHARS.all,
285
310
  min_len: int = None,
286
311
  max_len: int = None,
287
312
  mask_char: str = None,
288
313
  reset_ansi: bool = True,
289
- ) -> str | None:
314
+ ) -> Optional[str]:
290
315
  """Acts like a standard Python `input()` with the advantage, that you can specify:
291
316
  - what text characters the user is allowed to type and
292
317
  - the minimum and/or maximum length of the users input
293
318
  - optional mask character (hide user input, e.g. for passwords)
294
319
  - reset the ANSI formatting codes after the user continues\n
295
- -----------------------------------------------------------------------------------
320
+ ---------------------------------------------------------------------------------------
296
321
  The input can be formatted with special formatting codes. For more detailed
297
- information about formatting codes, see the `xx_format_codes` description."""
322
+ information about formatting codes, see the `xx_format_codes` module documentation."""
298
323
  FormatCodes.print(start + prompt, default_color=default_color, end="")
299
324
  result = ""
300
325
  select_all = False
@@ -303,22 +328,17 @@ class Console:
303
328
 
304
329
  def update_display(console_width: int) -> None:
305
330
  nonlocal select_all, last_line_count, last_console_width
306
- lines = String.split_count(
307
- str(prompt) + (mask_char * len(result) if mask_char else result),
308
- console_width,
309
- )
331
+ lines = String.split_count(str(prompt) + (mask_char * len(result) if mask_char else result), console_width)
310
332
  line_count = len(lines)
311
333
  if (line_count > 1 or line_count < last_line_count) and not last_line_count == 1:
312
334
  if last_console_width > console_width:
313
335
  line_count *= 2
314
- for _ in range(
315
- line_count
316
- if line_count < last_line_count and not line_count > last_line_count
317
- else (line_count - 2 if line_count > last_line_count else line_count - 1)
318
- ):
336
+ for _ in range(line_count if line_count < last_line_count and not line_count > last_line_count else (
337
+ line_count - 2 if line_count > last_line_count else line_count - 1)):
319
338
  _sys.stdout.write("\033[2K\r\033[A")
320
339
  prompt_len = len(str(prompt)) if prompt else 0
321
- prompt_str, input_str = lines[0][:prompt_len], (
340
+ prompt_str = lines[0][:prompt_len]
341
+ input_str = (
322
342
  lines[0][prompt_len:] if len(lines) == 1 else "\n".join([lines[0][prompt_len:]] + lines[1:])
323
343
  ) # SEPARATE THE PROMPT AND THE INPUT
324
344
  _sys.stdout.write(
@@ -338,7 +358,7 @@ class Console:
338
358
  result, select_all = "", False
339
359
  elif result and event.name == "backspace":
340
360
  result = result[:-1]
341
- update_display(Console.w())
361
+ update_display(Console.w)
342
362
 
343
363
  def handle_paste():
344
364
  nonlocal result, select_all
@@ -347,25 +367,18 @@ class Console:
347
367
  filtered_text = "".join(char for char in _pyperclip.paste() if allowed_chars == CHARS.all or char in allowed_chars)
348
368
  if max_len is None or len(result) + len(filtered_text) <= max_len:
349
369
  result += filtered_text
350
- update_display(Console.w())
370
+ update_display(Console.w)
351
371
 
352
372
  def handle_select_all():
353
373
  nonlocal select_all
354
374
  select_all = True
355
- update_display(Console.w())
356
-
357
- def handle_copy():
358
- nonlocal select_all
359
- with suppress(KeyboardInterrupt):
360
- select_all = False
361
- update_display(Console.w())
362
- _pyperclip.copy(result)
375
+ update_display(Console.w)
363
376
 
364
377
  def handle_character_input():
365
378
  nonlocal result
366
379
  if (allowed_chars == CHARS.all or event.name in allowed_chars) and (max_len is None or len(result) < max_len):
367
380
  result += event.name
368
- update_display(Console.w())
381
+ update_display(Console.w)
369
382
 
370
383
  while True:
371
384
  event = _keyboard.read_event()
@@ -378,8 +391,8 @@ class Console:
378
391
  handle_paste()
379
392
  elif event.name == "a" and _keyboard.is_pressed("ctrl"):
380
393
  handle_select_all()
381
- elif event.name == "c" and _keyboard.is_pressed("ctrl") and select_all:
382
- handle_copy()
394
+ elif event.name == "c" and _keyboard.is_pressed("ctrl"):
395
+ raise KeyboardInterrupt
383
396
  elif event.name == "esc":
384
397
  return None
385
398
  elif event.name == "space":
@@ -388,19 +401,54 @@ class Console:
388
401
  handle_character_input()
389
402
  else:
390
403
  select_all = False
391
- update_display(Console.w())
404
+ update_display(Console.w)
392
405
 
393
406
  @staticmethod
394
407
  def pwd_input(
395
408
  prompt: object = "Password: ",
396
409
  start="",
397
410
  end="\n",
398
- default_color: hexa | rgba = DEFAULT.color["cyan"],
411
+ default_color: hexa | rgba = COLOR.cyan,
399
412
  allowed_chars: str = CHARS.standard_ascii,
400
413
  min_len: int = None,
401
414
  max_len: int = None,
402
- _reset_ansi: bool = True,
415
+ reset_ansi: bool = True,
403
416
  ) -> str:
404
417
  """Password input (preset for `Console.restricted_input()`)
405
418
  that always masks the entered characters with asterisks."""
406
- return Console.restricted_input(prompt, start, end, default_color, allowed_chars, min_len, max_len, "*", _reset_ansi)
419
+ return Console.restricted_input(prompt, start, end, default_color, allowed_chars, min_len, max_len, "*", reset_ansi)
420
+
421
+ @staticmethod
422
+ def multiline_input(
423
+ prompt: object = "",
424
+ start="",
425
+ end="\n",
426
+ default_color: hexa | rgba = COLOR.cyan,
427
+ show_keybindings=True,
428
+ input_prefix=" ⤷ ",
429
+ reset_ansi=True,
430
+ ) -> str:
431
+ """An input where users can input (and paste) text over multiple lines.\n
432
+ -----------------------------------------------------------------------------------
433
+ - `prompt` -⠀the input prompt
434
+ - `start` -⠀something to print before the input
435
+ - `end` -⠀something to print after the input (e.g. `\\n`)
436
+ - `default_color` -⠀the default text color of the `prompt`
437
+ - `show_keybindings` -⠀whether to show the special keybindings or not
438
+ - `input_prefix` -⠀the prefix of the input line
439
+ - `reset_ansi` -⠀whether to reset the ANSI codes after the input or not
440
+ -----------------------------------------------------------------------------------
441
+ The input prompt can be formatted with special formatting codes. For more detailed
442
+ information about formatting codes, see `xx_format_codes` module documentation."""
443
+ kb = KeyBindings()
444
+
445
+ @kb.add("c-d", eager=True) # CTRL+D
446
+ def _(event):
447
+ event.app.exit(result=event.app.current_buffer.document.text)
448
+
449
+ FormatCodes.print(start + prompt, default_color=default_color)
450
+ if show_keybindings:
451
+ FormatCodes.print("[dim][[b](CTRL+D)[dim] : end of input][_dim]")
452
+ input_string = _prompt_toolkit.prompt(input_prefix, multiline=True, wrap_lines=True, key_bindings=kb)
453
+ FormatCodes.print("[_]" if reset_ansi else "", end=end[1:] if end.startswith("\n") else end)
454
+ return input_string