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