Module ploigos_step_runner.utils.io
Shared utilities for dealing with IO
Functions
def create_sh_redirect_to_multiple_streams_fn_callback(streams)
-
Creates and returns a function callback that will write given data to multiple given streams.
AKA: this essentially allows you to do 'tee' for sh commands.
Parameters
streams
:list
ofio.IOBase
- Streams to write to.
Examples
Will write output directed at stdout to stdout and a results file and output directed at stderr to stderr and a results file.
>>> with open('/tmp/results_file', 'w') as results_file: ... out_callback = create_sh_redirect_to_multiple_streams_fn_callback([ ... sys.stdout, ... results_file ... ]) ... err_callback = create_sh_redirect_to_multiple_streams_fn_callback([ ... sys.stderr, ... results_file ... ]) ... sh.echo('hello world') hello world
Returns
function(data)
- Function that takes one parameter, data, and writes that value to all the given streams.
Classes
class TextIOIndenter (parent_stream, indent_level=0, indent_size=4, indent_char=' ')
-
Adds an indent to the first string written and after every new line written to this stream.
Notes
WARNING: There will be a dangling indent after the last new line written to this stream because there is no "good" way of knowing the last new line ever written to this stream.
Parameters
parent_stream
:IOBase
- Stream to write to after indenting what is written to this stream
indent_level
:int
, optional- Level to indent to. Will be multiplied by the indent_size.
indent_size
:int
, optional- Size of each indent. Will be multipled by the indent_level.
indent_char
:str
, optional- Character to use for indent. Will be multiplied by the indent_size and the ident_level and prepended before each line written to this stream.
Expand source code
class TextIOIndenter(io.TextIOBase): """Adds an indent to the first string written and after every new line written to this stream. Notes ----- WARNING: There will be a dangling indent after the last new line written to this stream because there is no "good" way of knowing the last new line ever written to this stream. Parameters ---------- parent_stream : IOBase Stream to write to after indenting what is written to this stream indent_level : int, optional Level to indent to. Will be multiplied by the indent_size. indent_size : int, optional Size of each indent. Will be multipled by the indent_level. indent_char : str, optional Character to use for indent. Will be multiplied by the indent_size and the ident_level and prepended before each line written to this stream. """ def __init__(self, parent_stream, indent_level=0, indent_size=4, indent_char=' '): self.__parent_stream = parent_stream self.__indent_level = indent_level self.__indent_size = indent_size self.__indent_char = indent_char self.__unwritten_to = True super().__init__() @property def parent_stream(self): """Returns parent stream that this stream wraps Returns ------- IOBase Stream to write to after indenting what is written to this stream """ return self.__parent_stream @property def indent_level(self): """Level to indent to. Returns ------- int Level to indent to. """ return self.__indent_level @property def indent_size(self): """Size of each indent. Returns ------- int Size of each indent. """ return self.__indent_size @property def indent_char(self): """Character to use for indent. Will be multiplied by the indent_size and the ident_level and prepended before each line written to this stream. Returns ------- str Character to use for indent. """ return self.__indent_char def write(self, given): """Indents the begining of the given text as well as every new line in the given text and then writes it to this steams parent_stream. Notes ----- If there is a new line at the end of the given string there will not be an indent written after that new line assuming that the next line will be written by this stream and therefor get indented as being initially written to this stream. Parameters ---------- given : str or bytes (utf-8) String to indent every line of before writing to parent stream. Examples -------- >>> TextIOIndenter(sys.stdout).write("hello world\\n") hello world <BLANKLINE> >>> TextIOIndenter(sys.stdout, 1).write("hello world\\n") hello world <BLANKLINE> >>> TextIOIndenter(sys.stdout, 2).write("hello world\\n") hello world <BLANKLINE> >>> TextIOIndenter(sys.stdout).write("\\nhello world\\n") <BLANKLINE> hello world <BLANKLINE> >>> TextIOIndenter(sys.stdout, 1).write("\\nhello world\\n") <BLANKLINE> hello world <BLANKLINE> >>> TextIOIndenter(sys.stdout, 2).write("\\nhello world\\n") <BLANKLINE> hello world <BLANKLINE> >>> TextIOIndenter(sys.stdout, 0, 2, '-').write("hello world") hello world >>> TextIOIndenter(sys.stdout, 1, 2, '-').write("hello world") --hello world >>> TextIOIndenter(sys.stdout, 2, 2, '-').write(hello world") ----hello world >>> TextIOIndenter(sys.stdout, 1).write("hello\\nworld\\n") hello world <BLANKLINE> >>> TextIOIndenter(sys.stdout, 2).write("hello\\nworld\\n") hello world <BLANKLINE> >>> indenter = TextIOIndenter(sys.stdout, 1) ... indenter.write("hello\\nworld\\n") ... indenter.write("foo bar\\n") hello world foo bar >>> indenter = TextIOIndenter(sys.stdout, 1) ... indenter.write("hello world ") ... indenter.write("foo bar\\n") ... indenter.write("this is a test, ") ... indenter.write("more testing\\n") ... indenter.write("fortytwo\\n") hello world foo bar this is a test more testing fortytwo Returns ------- int Number of characters written. See Also -------- io.TextIOBase.write """ if isinstance(given, bytes): indented = given.decode('utf-8') else: indented = given # create the indent to insert at begining of given text and every new line in that text indent_chars = self.indent_char * (self.indent_size * self.indent_level) if self.__unwritten_to: self.__unwritten_to = False indented = f"{indent_chars}{indented}" # add indent after every new line # NOTE: \1 is capture group one and contains the original new line character indented = re.sub(r"(\r\n|\r|\n)", r"\1" + indent_chars, indented) return self.parent_stream.write(indented) def flush(self): """Flush the parent stream. See Also -------- io.TextIOBase.flush """ self.parent_stream.flush()
Ancestors
- io.TextIOBase
- _io._TextIOBase
- io.IOBase
- _io._IOBase
Instance variables
prop indent_char
-
Character to use for indent.
Will be multiplied by the indent_size and the ident_level and prepended before each line written to this stream.
Returns
str
- Character to use for indent.
Expand source code
@property def indent_char(self): """Character to use for indent. Will be multiplied by the indent_size and the ident_level and prepended before each line written to this stream. Returns ------- str Character to use for indent. """ return self.__indent_char
prop indent_level
-
Level to indent to.
Returns
int
- Level to indent to.
Expand source code
@property def indent_level(self): """Level to indent to. Returns ------- int Level to indent to. """ return self.__indent_level
prop indent_size
-
Size of each indent.
Returns
int
- Size of each indent.
Expand source code
@property def indent_size(self): """Size of each indent. Returns ------- int Size of each indent. """ return self.__indent_size
prop parent_stream
-
Returns parent stream that this stream wraps
Returns
IOBase
- Stream to write to after indenting what is written to this stream
Expand source code
@property def parent_stream(self): """Returns parent stream that this stream wraps Returns ------- IOBase Stream to write to after indenting what is written to this stream """ return self.__parent_stream
Methods
def flush(self)
-
Flush the parent stream.
See Also
io.TextIOBase.flush
def write(self, given)
-
Indents the begining of the given text as well as every new line in the given text and then writes it to this steams parent_stream.
Notes
If there is a new line at the end of the given string there will not be an indent written after that new line assuming that the next line will be written by this stream and therefor get indented as being initially written to this stream.
Parameters
given
:str
orbytes (utf-8)
- String to indent every line of before writing to parent stream.
Examples
>>> TextIOIndenter(sys.stdout).write("hello world\n") hello world <BLANKLINE>
>>> TextIOIndenter(sys.stdout, 1).write("hello world\n") hello world <BLANKLINE>
>>> TextIOIndenter(sys.stdout, 2).write("hello world\n") hello world <BLANKLINE>
>>> TextIOIndenter(sys.stdout).write("\nhello world\n") <BLANKLINE> hello world <BLANKLINE>
>>> TextIOIndenter(sys.stdout, 1).write("\nhello world\n") <BLANKLINE> hello world <BLANKLINE>
>>> TextIOIndenter(sys.stdout, 2).write("\nhello world\n") <BLANKLINE> hello world <BLANKLINE>
>>> TextIOIndenter(sys.stdout, 0, 2, '-').write("hello world") hello world
>>> TextIOIndenter(sys.stdout, 1, 2, '-').write("hello world") --hello world
>>> TextIOIndenter(sys.stdout, 2, 2, '-').write(hello world") ----hello world
>>> TextIOIndenter(sys.stdout, 1).write("hello\nworld\n") hello world <BLANKLINE>
>>> TextIOIndenter(sys.stdout, 2).write("hello\nworld\n") hello world <BLANKLINE>
>>> indenter = TextIOIndenter(sys.stdout, 1) ... indenter.write("hello\nworld\n") ... indenter.write("foo bar\n") hello world foo bar
>>> indenter = TextIOIndenter(sys.stdout, 1) ... indenter.write("hello world ") ... indenter.write("foo bar\n") ... indenter.write("this is a test, ") ... indenter.write("more testing\n") ... indenter.write("fortytwo\n") hello world foo bar this is a test more testing fortytwo
Returns
int
- Number of characters written.
See Also
io.TextIOBase.write
class TextIOSelectiveObfuscator (parent_stream, randomize_replacment_length=True, replacement_char='*')
-
Extends the base class for text streams to allow the obfuscation of given patterns.
This is useful to prevent accidentally writing "sensitive" information to stdout/stderr.
Parameters
parent_stream
:IOBase
- IO stream to write to after obfuscating any text written to this stream.
randomize_replacment_length
:bool
, optional- True to randomize the length of the text being obfuscated in the stream. False to use the same length replacement for any obfuscated text in the stream.
replacement_char
:char
- Character to replace the target strings to obfuscate with.
Attributes
__parent_stream
:IOBase
__obfuscation_patterns
:list
__replacement_char
:char
__randomize_replacement_length
:bool
__random_replacement_length_min
:int
__random_replacement_length_max
:int
Expand source code
class TextIOSelectiveObfuscator(io.TextIOBase): """Extends the base class for text streams to allow the obfuscation of given patterns. This is useful to prevent accidentally writing "sensitive" information to stdout/stderr. Parameters ---------- parent_stream : IOBase IO stream to write to after obfuscating any text written to this stream. randomize_replacment_length : bool, optional True to randomize the length of the text being obfuscated in the stream. False to use the same length replacement for any obfuscated text in the stream. replacement_char : char Character to replace the target strings to obfuscate with. Attributes ---------- __parent_stream : IOBase __obfuscation_patterns : list __replacement_char : char __randomize_replacement_length : bool __random_replacement_length_min : int __random_replacement_length_max : int """ def __init__(self, parent_stream, randomize_replacment_length=True, replacement_char='*'): self.__parent_stream = parent_stream self.__obfuscation_patterns = [] self.__replacement_char = replacement_char self.__randomize_replacement_length = randomize_replacment_length self.__random_replacement_length_min = 5 self.__random_replacement_length_max = 40 super().__init__() @property def parent_stream(self): """ Returns ------- IOBase IO stream this stream writes to after obfuscating any text written to this stream. """ return self.__parent_stream @property def replacement_char(self): """ Returns ------- char Character to replace the target strings to obfuscate with. """ return self.__replacement_char @replacement_char.setter def replacement_char(self, replacement_char): """ Parameters ---------- replacement_char : char Character to replace the target strings to obfuscate with. """ self.__replacement_char = replacement_char @property def randomize_replacement_length(self): """ Returns ------- bool True if this stream is randomizing the length of the text being obfuscated. False if this stream is using the same length replacement for any obfuscated text. """ return self.__randomize_replacement_length def add_obfuscation_targets(self, targets): """Adds a target pattern to be obfuscated whenever writing to this stream. Notes ----- This is a bit involved to deal with secrets that span multiple lines and various ways they can be printed. so the regex gets pretty involved to escape the right things and ignore whitespace, so forth and so on. There are unit tests covering the scenarios this is dealing with, if you are messing in here be sure you don't break any of the existing unit tests. Parameters ---------- targets : list or pattern The target patterns to be obfuscated when writing to this stream. """ if not isinstance(targets, list): targets = [targets] for target in targets: target_pattern = target # replace any amount of whitespace with a single space target_pattern = re.sub(r'\s+', ' ', target_pattern) # strip off leading and trialing whitespace target_pattern = target_pattern.strip() # escape for use in regex pattern target_pattern = re.escape(target_pattern) # the spaces we added in now got escaped, so unescape them and turn them into .* target_pattern = re.sub(r'\\ ', r'.*', target_pattern) # eat up pre and post newlines # target_pattern = f"(\s*)({target_pattern})(\s*)" # compile the pattern for re-use and make sure that .* matches accross lines target_compiled_pattern = re.compile(target_pattern, re.DOTALL) # add the pattern self.__obfuscation_patterns.append(target_compiled_pattern) def __obfuscator(self, match): """Given a regex match returns a corresponding obfuscated string. Parameters ---------- match : re.Match re.Match to replace with obfuscated text Returns ------- str String to replace the given match with. Also See -------- re.sub """ if self.randomize_replacement_length: replacement_length = random.randint( self.__random_replacement_length_min, self.__random_replacement_length_max ) else: replacement_length = len(match.group()) return self.replacement_char * replacement_length def write(self, given): """Writes to this streams parent stream after obfuscating all of the obfuscation targets. Parameters ---------- given : str Given string to write to the parent stream after obfuscating. Returns ------- int Number of characters written. See Also -------- io.TextIOBase.write """ if isinstance(given, bytes): obfuscated = given.decode('utf-8') else: obfuscated = given for obfuscation_pattern in self.__obfuscation_patterns: obfuscated = obfuscation_pattern.sub(self.__obfuscator, obfuscated) return self.parent_stream.write(obfuscated) def flush(self): """Flush the parent stream. See Also -------- io.TextIOBase.flush """ self.parent_stream.flush()
Ancestors
- io.TextIOBase
- _io._TextIOBase
- io.IOBase
- _io._IOBase
Instance variables
prop parent_stream
-
Returns
IOBase
- IO stream this stream writes to after obfuscating any text written to this stream.
Expand source code
@property def parent_stream(self): """ Returns ------- IOBase IO stream this stream writes to after obfuscating any text written to this stream. """ return self.__parent_stream
prop randomize_replacement_length
-
Returns
bool
- True if this stream is randomizing the length of the text being obfuscated. False if this stream is using the same length replacement for any obfuscated text.
Expand source code
@property def randomize_replacement_length(self): """ Returns ------- bool True if this stream is randomizing the length of the text being obfuscated. False if this stream is using the same length replacement for any obfuscated text. """ return self.__randomize_replacement_length
prop replacement_char
-
Returns
char
- Character to replace the target strings to obfuscate with.
Expand source code
@property def replacement_char(self): """ Returns ------- char Character to replace the target strings to obfuscate with. """ return self.__replacement_char
Methods
def add_obfuscation_targets(self, targets)
-
Adds a target pattern to be obfuscated whenever writing to this stream.
Notes
This is a bit involved to deal with secrets that span multiple lines and various ways they can be printed. so the regex gets pretty involved to escape the right things and ignore whitespace, so forth and so on.
There are unit tests covering the scenarios this is dealing with, if you are messing in here be sure you don't break any of the existing unit tests.
Parameters
targets
:list
orpattern
- The target patterns to be obfuscated when writing to this stream.
def flush(self)
-
Flush the parent stream.
See Also
io.TextIOBase.flush
def write(self, given)
-
Writes to this streams parent stream after obfuscating all of the obfuscation targets.
Parameters
given
:str
- Given string to write to the parent stream after obfuscating.
Returns
int
- Number of characters written.
See Also
io.TextIOBase.write