datatypes 0.31.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 (53) hide show
  1. datatypes/__init__.py +169 -0
  2. datatypes/collections/__init__.py +25 -0
  3. datatypes/collections/container.py +248 -0
  4. datatypes/collections/mapping.py +1226 -0
  5. datatypes/collections/sequence.py +311 -0
  6. datatypes/command.py +596 -0
  7. datatypes/compat.py +206 -0
  8. datatypes/config/__init__.py +13 -0
  9. datatypes/config/environ/__init__.py +33 -0
  10. datatypes/config/environ/base.py +332 -0
  11. datatypes/config/environ/token.py +173 -0
  12. datatypes/config/parser.py +256 -0
  13. datatypes/config/settings.py +352 -0
  14. datatypes/copy.py +151 -0
  15. datatypes/csv.py +547 -0
  16. datatypes/datetime.py +1243 -0
  17. datatypes/decorators/__init__.py +31 -0
  18. datatypes/decorators/base.py +449 -0
  19. datatypes/decorators/descriptor.py +490 -0
  20. datatypes/decorators/misc.py +152 -0
  21. datatypes/email.py +439 -0
  22. datatypes/enum.py +176 -0
  23. datatypes/event.py +299 -0
  24. datatypes/exception.py +13 -0
  25. datatypes/filetypes/__init__.py +10 -0
  26. datatypes/filetypes/html.py +540 -0
  27. datatypes/filetypes/toml.py +275 -0
  28. datatypes/http.py +1443 -0
  29. datatypes/logging.py +475 -0
  30. datatypes/number.py +586 -0
  31. datatypes/parse.py +362 -0
  32. datatypes/path.py +4358 -0
  33. datatypes/profile.py +246 -0
  34. datatypes/reflection/__init__.py +24 -0
  35. datatypes/reflection/inspect.py +3475 -0
  36. datatypes/reflection/other.py +843 -0
  37. datatypes/reflection/path.py +639 -0
  38. datatypes/server.py +642 -0
  39. datatypes/service.py +212 -0
  40. datatypes/ssh.py +812 -0
  41. datatypes/string.py +1632 -0
  42. datatypes/token/__init__.py +16 -0
  43. datatypes/token/abnf.py +1696 -0
  44. datatypes/token/base.py +921 -0
  45. datatypes/token/word.py +259 -0
  46. datatypes/typing.py +126 -0
  47. datatypes/url.py +988 -0
  48. datatypes/utils.py +179 -0
  49. datatypes-0.31.0.dist-info/METADATA +38 -0
  50. datatypes-0.31.0.dist-info/RECORD +53 -0
  51. datatypes-0.31.0.dist-info/WHEEL +5 -0
  52. datatypes-0.31.0.dist-info/licenses/LICENSE.txt +21 -0
  53. datatypes-0.31.0.dist-info/top_level.txt +1 -0
datatypes/command.py ADDED
@@ -0,0 +1,596 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import subprocess
4
+ from subprocess import (
5
+ #CalledProcessError,
6
+ SubprocessError,
7
+ TimeoutExpired,
8
+ )
9
+ import sys
10
+ import re
11
+ import signal
12
+ import time
13
+ import logging
14
+ from collections import deque
15
+ import threading
16
+
17
+ from .compat import *
18
+ from .collections import Dict
19
+ from .utils import make_list
20
+ from .decorators import property as cachedproperty
21
+ from .string import String
22
+ from .exception import CalledProcessError
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class CaptureProcess(subprocess.Popen):
29
+ """Handle stream capturing
30
+
31
+ Our default is to capture both stdout and stderr. We change the default
32
+ behavior a little bit in that None will be treated as /dev/null
33
+
34
+ From the docs:
35
+ If you wish to capture and combine both streams into one,
36
+ use stdout=PIPE and stderr=STDOUT
37
+
38
+ Valid values are PIPE, DEVNULL, an existing file descriptor (a
39
+ positive integer), an existing file object with a valid file
40
+ descriptor, and None. PIPE indicates that a new pipe to the child
41
+ should be created. DEVNULL indicates that the special file os.devnull
42
+ will be used.
43
+
44
+ Additionally, stderr can be STDOUT, which indicates that the stderr
45
+ data from the child process should be captured into the same file
46
+ handle as for stdout.
47
+
48
+ https://docs.python.org/3/library/subprocess.html#subprocess.Popen
49
+ """
50
+ def __init__(self, *args, **kwargs):
51
+ kwargs.setdefault("stderr", subprocess.STDOUT)
52
+ kwargs.setdefault("stdout", subprocess.PIPE)
53
+ self.pipe_name = "stdout"
54
+
55
+ if kwargs["stderr"] and not kwargs["stdout"]:
56
+ kwargs["stderr"] = subprocess.PIPE
57
+ self.pipe_name = "stderr"
58
+
59
+ elif kwargs["stderr"] and kwargs["stdout"]:
60
+ kwargs["stderr"] = subprocess.STDOUT
61
+
62
+ if not kwargs["stderr"]:
63
+ kwargs["stderr"] = subprocess.DEVNULL
64
+
65
+ if not kwargs["stdout"]:
66
+ kwargs["stdout"] = subprocess.DEVNULL
67
+
68
+ # https://docs.python.org/3/library/collections.html#collections.deque
69
+ # Deques support thread-safe, memory efficient appends and pops
70
+ self.deque = deque(maxlen=kwargs.pop("bufsize", 1000))
71
+ self.returncodes = kwargs.pop("returncodes", [0])
72
+ self.returncode = None
73
+ super().__init__(*args, **kwargs)
74
+
75
+ def __iter__(self):
76
+ output = getattr(self, self.pipe_name)
77
+ try:
78
+ # another round of links
79
+ # http://stackoverflow.com/a/17413045/5006 (what I used)
80
+ # http://stackoverflow.com/questions/2715847/
81
+ #for line in iter(self.stdout.readline, b""):
82
+ for line in iter(output.readline, b""):
83
+ line = line.decode("utf-8")
84
+ self.deque.append(line)
85
+ yield line
86
+
87
+ finally:
88
+ output.close()
89
+
90
+ def is_running(self):
91
+ return self.poll() is None
92
+
93
+ def check_returncode(self, returncodes=None):
94
+ """
95
+ https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess.check_returncode
96
+ """
97
+ if not returncodes:
98
+ returncodes = self.returncodes
99
+ returncodes = set(make_list(returncodes))
100
+
101
+ if self.returncode is not None:
102
+ if self.returncode not in returncodes:
103
+ raise CalledProcessError(
104
+ self.returncode,
105
+ self.args,
106
+ "".join(self.deque),
107
+ )
108
+
109
+ # raise CalledProcessError(
110
+ # "{} returned {}, expected one of {}".format(
111
+ # self.args,
112
+ # self.returncode,
113
+ # returncodes,
114
+ # ),
115
+ # self.args,
116
+ # )
117
+
118
+
119
+ class SimpleCommand(object):
120
+ """makes running a command from a non CLI environment easy peasy
121
+
122
+ This is handy when you need to test some CLI functionality of your python
123
+ modules.
124
+
125
+ https://docs.python.org/3/library/subprocess.html
126
+
127
+ Moved here from testdata.client.Command on 7-9-2023
128
+
129
+ :Example:
130
+ c = Command("echo")
131
+ r = c.run("hello")
132
+ print(r) # hello
133
+ """
134
+ quiet = False
135
+ """this is the default quiet setting for running a script, it can
136
+ be overriden in run()"""
137
+
138
+ process_class = CaptureProcess
139
+ """The class that will be used in .create_process, needs to be a subclass
140
+ of Popen, honestly, it will have to be a subclass of CaptureProcess since
141
+ it relies on the deque property and check_returncode method
142
+
143
+ https://docs.python.org/3/library/subprocess.html#subprocess.Popen
144
+ """
145
+ @cachedproperty(cached="_environ")
146
+ def environ(self):
147
+ environ = dict(os.environ)
148
+ pythonpath = self.cwd
149
+ if "PYTHONPATH" in environ:
150
+ environ["PYTHONPATH"] += os.pathsep + pythonpath
151
+ else:
152
+ environ["PYTHONPATH"] = pythonpath
153
+
154
+ if os.getcwd() not in environ["PYTHONPATH"]:
155
+ environ["PYTHONPATH"] += os.pathsep + os.getcwd()
156
+
157
+ return environ
158
+
159
+ def __init__(self, command, cwd="", environ=None, **kwargs):
160
+ self.cwd = os.path.realpath(cwd) if cwd else os.getcwd()
161
+ self.command = command
162
+
163
+ self.logger_prefix = kwargs.pop("logger_prefix", None)
164
+ if "logger" in kwargs:
165
+ self.logger = kwargs.pop("logger")
166
+
167
+ else:
168
+ self.logger = self.get_logger(**kwargs)
169
+
170
+ self.sudo = kwargs.pop("sudo", False)
171
+
172
+ if environ is not None:
173
+ self.environ = environ
174
+
175
+ self.kwargs = kwargs
176
+
177
+ def _flush(self, process, line):
178
+ """flush the line to stdout"""
179
+ if not process.quiet:
180
+ if self.logger_prefix is None:
181
+ self.logger_prefix = "{:0>5}: ".format(process.pid)
182
+ self.logger.info("{}{}".format(self.logger_prefix, line.rstrip()))
183
+
184
+ def get_logger(self, **kwargs):
185
+ """Get/create the command logger
186
+
187
+ The command logger is named: datatypes.command.Command
188
+
189
+ The command logger is used in ._flush() and it doesn't propogate or
190
+ inherit like standard loggers
191
+
192
+ :returns: logging.Logger
193
+ """
194
+ logger = logging.getLogger(f"{__name__}.Command")
195
+ if len(logger.handlers) == 0:
196
+ if logger.level == logging.NOTSET:
197
+ logger.setLevel(logging.INFO)
198
+ log_handler = logging.StreamHandler(stream=sys.stdout)
199
+ log_handler.setFormatter(logging.Formatter('%(message)s'))
200
+ logger.addHandler(log_handler)
201
+ logger.propagate = False
202
+ return logger
203
+
204
+ def create_cmd(self, command, arg_str):
205
+ if command:
206
+ if isinstance(command, basestring):
207
+ cmd = command
208
+ if arg_str:
209
+ if isinstance(arg_str, basestring):
210
+ cmd += " " + arg_str
211
+
212
+ else:
213
+ cmd += " ".join(arg_str)
214
+
215
+ else:
216
+ cmd = list(command)
217
+ if arg_str:
218
+ if isinstance(arg_str, basestring):
219
+ cmd.append(arg_str)
220
+
221
+ else:
222
+ cmd.extend(arg_str)
223
+
224
+ else:
225
+ cmd = arg_str
226
+
227
+ if self.sudo:
228
+ if isinstance(cmd, basestring):
229
+ if not cmd.startswith("sudo"):
230
+ cmd = "sudo " + cmd
231
+
232
+ else:
233
+ if not cmd[0] == "sudo":
234
+ cmd.insert(0, "sudo")
235
+
236
+ return cmd
237
+
238
+ def create_process(self, arg_str, **kwargs):
239
+ """
240
+ https://docs.python.org/3/library/subprocess.html
241
+ """
242
+ process = None
243
+ cmd = self.create_cmd(self.command, arg_str)
244
+
245
+ kwargs = Dict(self.kwargs, kwargs)
246
+ quiet = kwargs.pop("quiet", self.quiet)
247
+
248
+ retcode = kwargs.pops([
249
+ "returncodes",
250
+ "returncode",
251
+ "code",
252
+ "ret_code",
253
+ "retcode",
254
+ "expected_ret_code",
255
+ "expected_returncode",
256
+ "codes",
257
+ "ret_codes",
258
+ "retcodes",
259
+ "expected_ret_codes",
260
+ "expected_returncodes",
261
+ ], 0)
262
+ kwargs["returncodes"] = retcode
263
+
264
+ environ = self.environ
265
+ environ.update(kwargs.pop("environ", {}))
266
+ environ.update(kwargs.pop("env", {}))
267
+ # any kwargs with all capital letters should be considered environment
268
+ # variables
269
+ for k in list(kwargs.keys()):
270
+ if k.isupper():
271
+ environ[k] = kwargs.pop(k)
272
+
273
+ # make sure each value is a string
274
+ for k in environ.keys():
275
+ if not isinstance(environ[k], basestring):
276
+ environ[k] = String(environ[k])
277
+
278
+ # we will not allow these to be overridden via kwargs
279
+ kwargs["shell"] = isinstance(cmd, basestring)
280
+ kwargs["cwd"] = self.cwd
281
+ kwargs["env"] = environ
282
+
283
+ try:
284
+ process = self.process_class(
285
+ cmd,
286
+ **kwargs
287
+ )
288
+
289
+ except CalledProcessError as e:
290
+ process.returncode = e.returncode
291
+
292
+ finally:
293
+ if process:
294
+ process.quiet = quiet
295
+
296
+ return process
297
+
298
+ def run(self, arg_str="", **kwargs):
299
+ """runs the passed in arguments
300
+
301
+ :param arg_str: string, the argument flags that will be passed to the
302
+ command
303
+ :param **kwargs: These will be passed to subprocess or consumed
304
+ :returns: string, the string of the output and will have .returncode
305
+ attribute
306
+ """
307
+ process = self.create_process(arg_str, **kwargs)
308
+
309
+ # consume all the output from the process
310
+ for line in process:
311
+ self._flush(process, line)
312
+
313
+ process.wait()
314
+ process.check_returncode()
315
+
316
+ # we wrap the output in a String so we can set returncode
317
+ ret = String("".join(process.deque))
318
+ ret.returncode = process.returncode
319
+ return ret
320
+
321
+
322
+ class Command(SimpleCommand):
323
+ """Runs a command in a thread when you call .start and syncronously when
324
+ .run is called
325
+
326
+ ??? - Could I switch to using ThreadPoolExecutor instead?
327
+ https://docs.python.org/3/library/concurrent.futures.html
328
+
329
+ :Example:
330
+ c = Command("<SOME COMMAND>")
331
+ c.start()
332
+ while r := c.wait(1):
333
+ if "<SOME SENTINAL STRING>" in r:
334
+ break
335
+ """
336
+ process = None
337
+ """Will hold the process that's started when .start() is called"""
338
+
339
+ thread_class = threading.Thread
340
+ """the threading class to use when .start is called"""
341
+
342
+ def is_running(self):
343
+ """True if the process is running"""
344
+ return (self.process.poll() is None) if self.process else False
345
+
346
+ def quit(self, timeout=1):
347
+ """same as .terminate but uses signals"""
348
+ return self.finish(
349
+ "send_signal",
350
+ timeout=timeout,
351
+ args=(signal.SIGTERM,)
352
+ )
353
+
354
+ def kill(self, timeout=1):
355
+ """kill -9 the script running asyncronously"""
356
+ return self.finish("kill", timeout=timeout)
357
+
358
+ def stop(self, timeout=1):
359
+ """This is just here because I use .start/.stop a lot and so it's
360
+ strange not having a .stop method"""
361
+ self.kill()
362
+
363
+ def terminate(self, timeout=1):
364
+ """terminate the script running asyncronously"""
365
+ return self.finish("terminate", timeout=timeout)
366
+
367
+ def murder(self, timeout=1, process_cmd=""):
368
+ """use pkill to kill any process that matches self.process.args"""
369
+ process_cmd = process_cmd or self.process.args
370
+ cmd = self.create_cmd(["pkill", "-9", "-f", process_cmd], "")
371
+ logger.debug("Murdering {}".format(process_cmd))
372
+ subprocess.run(cmd, check=False)
373
+ return self.wait(timeout)
374
+
375
+ def finish(self, method_name, timeout=1, maxcount=5, args=None, kwargs=None):
376
+ """Internal method that kills the async process, if it can't shut it
377
+ down soft it will shut it down hard
378
+
379
+ :param method_name: str, the method name that will first be attempted
380
+ :param timeout: int, how long to wait for each attempted method_name
381
+ call
382
+ :param maxcount: int, how many times to attempt calling method_name
383
+ :param *args: passed to method_name call
384
+ :param **kwargs: passed to method_name call
385
+ :returns: str, the buffer
386
+ """
387
+ ret = []
388
+
389
+ args = args or ()
390
+ kwargs = kwargs or {}
391
+ process = self.process
392
+
393
+ while self.is_running():
394
+ getattr(process, method_name)(*args, **kwargs)
395
+ ret.append(self.wait(timeout=timeout))
396
+ if process.returncode is None:
397
+ count += 1
398
+ if count >= maxcount:
399
+ ret.append(self.murder())
400
+
401
+ return "".join(ret)
402
+
403
+ def start(self, arg_str="", **kwargs):
404
+ """Run the command asyncronously, see parent's .run"""
405
+ self.process = self.create_process(arg_str, **kwargs)
406
+
407
+ def target():
408
+ for line in self.process:
409
+ self._flush(self.process, line)
410
+
411
+ t = self.thread_class(target=target)
412
+ t.daemon = True
413
+ t.start()
414
+ self.async_thread = t
415
+ return self.process
416
+
417
+ def join(self, *args, **kwargs): return self.wait(*args, **kwargs)
418
+ def wait(self, timeout=None):
419
+ """After .run is called you can call this to return current output from
420
+ when .run was called or when this method was last called
421
+
422
+ :param timeout: int, how long to wait
423
+ :returns: str, the output, the str also has a returncode value, which
424
+ will be None if the process isn't done yet
425
+ """
426
+ try:
427
+ self.process.wait(timeout=timeout)
428
+
429
+ except TimeoutExpired:
430
+ pass
431
+
432
+ retcode = self.process.returncode
433
+ if retcode is not None and retcode > 0:
434
+ self.process.check_returncode()
435
+
436
+ ret = "".join(self.process.deque)
437
+ self.process.deque.clear()
438
+
439
+ ret = String(ret)
440
+ ret.returncode = retcode
441
+
442
+ return ret
443
+
444
+ def wait_for(self, callback, timeout=0.1, maxcount=10):
445
+ """Wait until callback returns True or until maxcount * timeout is hit
446
+
447
+ :param callback: callable|str|re.Pattern, if a callable, then pass the
448
+ program output to callable until it returns True. If a string then
449
+ check string in output. If regex pattern then run search on output
450
+ :param timeout: float, each incremental timeout before next check
451
+ :param maxcount: int, the maximum times to check callback before
452
+ failing and just returning output
453
+ :returns str, the output, same as .wait()
454
+ """
455
+ if not callable(callback):
456
+ if isinstance(callback, str):
457
+ needle = callback
458
+ callback = lambda haystack: needle in haystack
459
+
460
+ elif isinstance(callback, re.Pattern):
461
+ regex = callback
462
+ callback = lambda haystack: regex.search(haystack)
463
+
464
+ else:
465
+ raise ValueError(f"Not sure what to do with {callback}")
466
+
467
+ haystack = String("")
468
+ haystack.returncode = None
469
+
470
+ count = 0
471
+ while count < maxcount:
472
+ count += 1
473
+
474
+ hay = self.wait(timeout=timeout)
475
+ haystack = String(haystack + hay)
476
+ haystack.returncode = hay.returncode
477
+
478
+ if callback(haystack):
479
+ break
480
+
481
+ if haystack.returncode is not None:
482
+ break
483
+
484
+ return haystack
485
+
486
+
487
+ class ModuleCommand(Command):
488
+ """This sets the client up so you can just pass the module name and have
489
+ everything just work
490
+
491
+ Moved here from testdata.client.Command on 7-12-2023
492
+
493
+ :Example:
494
+ #You can setup this command 2 ways
495
+
496
+ # 1 - pass in the module name
497
+ c = ModuleCommand("module_name")
498
+
499
+ # 2 - have the parent set it
500
+ class MyCommand(ModuleCommand):
501
+ name = "module_name"
502
+ c = MyCommand()
503
+ """
504
+ cmd_prefix = "{} -m".format(sys.executable)
505
+ """this is what will be used to invoke captain from the command line when
506
+ run() is called"""
507
+
508
+ name = ""
509
+ """This is the module name you want to run"""
510
+
511
+ def __init__(self, name="", cwd="", environ=None, **kwargs):
512
+ if name:
513
+ self.name = name
514
+
515
+ if not self.name:
516
+ raise ValueError("No name specified")
517
+
518
+ kwargs.setdefault("command", None)
519
+
520
+ super().__init__(
521
+ cwd=cwd,
522
+ environ=environ,
523
+ **kwargs
524
+ )
525
+
526
+ def create_cmd(self, command, arg_str):
527
+ if isinstance(command, basestring):
528
+ ret = "{} {} {}".format(self.cmd_prefix, self.name, command)
529
+
530
+ else:
531
+ ret = re.split(r"\s+", self.cmd_prefix)
532
+ ret.append(self.name)
533
+ if command:
534
+ ret.extend(command)
535
+
536
+ return super().create_cmd(ret, arg_str)
537
+
538
+
539
+ class FileCommand(ModuleCommand):
540
+ """This will add the .py to a script so you don't have to
541
+
542
+ Moved here from testdata.client.Command on 7-12-2023
543
+
544
+ an example might be best to understand how the command gets put together
545
+
546
+ Let's say you wanted to run this path:
547
+
548
+ /foo/bar/program.py
549
+
550
+ You could configure this class like this:
551
+
552
+ class ProgamCommand(FileCommand):
553
+ script_prefix = "/foo/bar"
554
+ script_postfix = ".py"
555
+ name = "program"
556
+
557
+ Then when you need to run program.py, you only need to do:
558
+
559
+ p = ProgramCommand()
560
+ p.run() # runs "python /foo/bar/program.py"
561
+
562
+ But you also could run it:
563
+
564
+ p = FileCommand("/foo/bar/program.py")
565
+
566
+ or:
567
+ class ProgamCommand(FileCommand):
568
+ script_prefix = "/foo/bar"
569
+
570
+ p = ProgramCommand("program.py")
571
+ """
572
+ cmd_prefix = sys.executable
573
+ """If you have a space in the executable path that might be really bad"""
574
+
575
+ script_prefix = ""
576
+ """this will be prepended to the passed in script on initialization"""
577
+
578
+ script_postfix = ""
579
+ """this will be appended to the passed in script on initialization"""
580
+
581
+ def __init__(self, fileroot="", cwd="", environ=None, **kwargs):
582
+ if fileroot:
583
+ self.name = fileroot
584
+
585
+ if not self.name:
586
+ raise ValueError("no name found")
587
+
588
+ path = self.name
589
+ if self.script_prefix and not fileroot.startswith(self.script_prefix):
590
+ path = os.path.join(self.script_prefix.rstrip("/"), fileroot)
591
+
592
+ if self.script_postfix and not path.endswith(self.script_postfix):
593
+ path += self.script_postfix
594
+
595
+ super().__init__(path, cwd=cwd, environ=environ, **kwargs)
596
+