utils.common

common

Common code useful for any pipeline

Example Usage: from utils import common from utils.common import subprocess_long

  1#!/usr/bin/env python3
  2"""
  3common
  4
  5Common code useful for any pipeline
  6
  7Example Usage:
  8    from utils import common
  9    from utils.common import subprocess_long
 10"""
 11
 12from subprocess_tee import run
 13import os
 14import re
 15import subprocess
 16from typing import TYPE_CHECKING, Any, List, Union
 17try:
 18    import loggy
 19except ImportError:
 20    from utils import loggy
 21
 22if TYPE_CHECKING:
 23    CompletedProcess = subprocess.CompletedProcess[Any]  # pylint: disable=E1136
 24else:
 25    CompletedProcess = subprocess.CompletedProcess
 26
 27try:
 28    from shlex import join  # type: ignore
 29except ImportError:
 30    from subprocess import list2cmdline as join  # pylint: disable=ungrouped-imports
 31
 32
 33class ChDir(object):
 34    """
 35    Step into a directory Temporarily
 36
 37    Usage Example:
 38        with ChDir("cdk"):
 39            loggy.info("I'm in the cdk folder")
 40    """
 41
 42    def __init__(self, path):
 43        self.old_dir = os.getcwd()
 44        self.new_dir = path if len(path) > 0 else os.getcwd()
 45
 46    def __enter__(self):
 47        os.chdir(self.new_dir)
 48
 49    def __exit__(self, *args):
 50        os.chdir(self.old_dir)
 51
 52
 53def removeprefix(s: str, prefix: str, /) -> str:
 54    """
 55    removeprefix()
 56
 57    This is built-in to string functions python3.9 but we use 3.8 for now.
 58
 59    s: String to check
 60    prefix: String prefix to look for
 61
 62    Returns: String modified with prefix removed
 63    """
 64    if s.startswith(prefix):
 65        return s[len(prefix):]
 66    else:
 67        return s[:]
 68
 69
 70def removesuffix(s: str, suffix: str, /) -> str:
 71    """
 72    removesuffix()
 73
 74    This is built-in to string functions python3.9 but we use 3.8 for now.
 75
 76    s: String to check
 77    suffix: String suffix to look for
 78
 79    Returns: String modified with suffix removed
 80    """
 81    if suffix and s.endswith(suffix):
 82        return s[:-len(suffix)]
 83    else:
 84        return s[:]
 85
 86
 87def resolve_gocd_pipeline_variable(param):
 88    """
 89    resolve_gocd_pipeline_variable()
 90
 91    GoCD pipeline variables get passed in as a literal string.
 92    Quickly check, resolve and pass back the true environment variable content.
 93    The `literal string` could be a variable surrounded by characters.
 94
 95    Examples:
 96        * $ENV_NAME
 97        * ${ENV_NAME}
 98        * my_${ENV_NAME}
 99        * ${ENV_NAME}_rc
100        * hello${ENV_NAME}goodbye
101
102    param: String containing potential env variable
103
104    Returns: String containing resolved env variable or original param if it can't resolve
105    """
106    _param = None
107
108    if "${" in param and "}" in param:
109        if param.startswith("${") and param.endswith("}"):
110            _param = removeprefix(param, "${")
111            _param = removesuffix(_param, "}")
112            _param = os.environ.get(_param, None)
113        else:
114            m = re.search('\\$\\{(.+?)\\}', param)
115            if m:
116                found = m.group(1)
117                _param = os.environ.get(found, None)
118                _param = _param if _param is None else param.replace("${" + found + "}", _param)
119    elif param.startswith("$"):
120        _param = removeprefix(param, "$")
121        _param = os.environ.get(_param, None)
122
123    return _param if _param is not None else param
124
125
126def subprocess_long(args: Union[str, List[str]], timeout=None, delay=None, check=None, shell=None, env=None):
127    """
128    subprocess_long():
129
130    imports subprocess_tee
131
132    Run long-running commands that are wrapped into a continuous loggy.info statement to keep
133    pipelines from exiting on zero output back to the GoCD server.
134
135    * Usage: common.subprocess_long(cmd="make install", timeout=15, delay=5, check=True)
136
137    timeout defaults to 30 minutes
138    delay defaults to 10 seconds
139    check defaults to False, use returned output.returncode to check for failure.
140        (Set to True if you want this function to error on command failure instead)
141    shell defaults to False
142    env defaults to None
143    """
144    timeout = 30 if timeout is None else timeout
145    delay = 10 if delay is None else delay
146    check = False if check is None else check
147    shell = False if shell is None else shell
148
149    if isinstance(args, str):
150        cmd = args
151    else:
152        # run was called with a list instead of a single item but asyncio
153        # create_subprocess_shell requires command as a single string, so
154        # we need to convert it to string
155        cmd = join(args)
156
157    longRunningProcessStart = 'counter=0; while [ $counter -lt ' + \
158        str(timeout*60) + \
159        ' ]; do counter=$((counter+' + \
160        str(delay) + \
161        ')); sleep ' + \
162        str(delay) + \
163        '; echo "common.subprocess_long(): LONG RUNNING PROCESS ENABLED - Running for $counter of ' + \
164        str(timeout*60) + \
165        ' seconds."; done & export _LONG_RUNNING_PROCESS=$!; '
166
167    longRunningProcessStop = '; retcode=$?; kill $_LONG_RUNNING_PROCESS; exit $retcode'
168
169    _process_output = run(longRunningProcessStart + cmd + longRunningProcessStop, env=env, shell=shell, check=check)
170
171    # loggy.info(_process_output.stdout)
172    # loggy.info(_process_output.stderr)
173    # loggy.info(_process_output.returncode)
174    return _process_output
175
176
177def subprocess_run(args: Union[str, List[str]], **kwargs: Any):
178    """
179    subprocess_run():
180
181    Replace the default subprocess_tee.run with check=True as default
182
183    * Usage: common.subprocess_run("make install")
184    """
185
186    if isinstance(args, str):
187        cmd = args
188    else:
189        # run was called with a list instead of a single item but asyncio
190        # create_subprocess_shell requires command as a single string, so
191        # we need to convert it to string
192        cmd = join(args)
193
194    try:
195        my_kwargs = kwargs.copy()
196        my_kwargs['check'] = kwargs.get("check", True)
197        my_kwargs['shell'] = kwargs.get("shell", True)
198        _process_output = run(cmd, **my_kwargs)
199    except subprocess.CalledProcessError as e:
200        loggy.error(f"common.subprocess_run(): Error: {str(e)}")
201        if _process_output and _process_output.stderr:
202            loggy.error(f"common.subprocess_run(): Process STDERR: {_process_output.stderr}")
203
204        raise
205
206    return _process_output
207
208
209def remove_empty_from_dict(d):
210    """
211    remove_empty_from_dict()
212
213    Removes empty values and empty lists from a dictionary
214
215    d: dict to modify
216
217    Returns: dict modified
218    """
219
220    if type(d) is dict:
221        return dict((k, remove_empty_from_dict(v)) for k, v in d.items() if v and remove_empty_from_dict(v))
222    elif type(d) is list:
223        return [remove_empty_from_dict(v) for v in d if v and remove_empty_from_dict(v)]
224    else:
225        return d
class ChDir:
34class ChDir(object):
35    """
36    Step into a directory Temporarily
37
38    Usage Example:
39        with ChDir("cdk"):
40            loggy.info("I'm in the cdk folder")
41    """
42
43    def __init__(self, path):
44        self.old_dir = os.getcwd()
45        self.new_dir = path if len(path) > 0 else os.getcwd()
46
47    def __enter__(self):
48        os.chdir(self.new_dir)
49
50    def __exit__(self, *args):
51        os.chdir(self.old_dir)

Step into a directory Temporarily

Usage Example: with ChDir("cdk"): loggy.info("I'm in the cdk folder")

ChDir(path)
43    def __init__(self, path):
44        self.old_dir = os.getcwd()
45        self.new_dir = path if len(path) > 0 else os.getcwd()
def removeprefix(s: str, prefix: str, /) -> str:
54def removeprefix(s: str, prefix: str, /) -> str:
55    """
56    removeprefix()
57
58    This is built-in to string functions python3.9 but we use 3.8 for now.
59
60    s: String to check
61    prefix: String prefix to look for
62
63    Returns: String modified with prefix removed
64    """
65    if s.startswith(prefix):
66        return s[len(prefix):]
67    else:
68        return s[:]

removeprefix()

This is built-in to string functions python3.9 but we use 3.8 for now.

s: String to check prefix: String prefix to look for

Returns: String modified with prefix removed

def removesuffix(s: str, suffix: str, /) -> str:
71def removesuffix(s: str, suffix: str, /) -> str:
72    """
73    removesuffix()
74
75    This is built-in to string functions python3.9 but we use 3.8 for now.
76
77    s: String to check
78    suffix: String suffix to look for
79
80    Returns: String modified with suffix removed
81    """
82    if suffix and s.endswith(suffix):
83        return s[:-len(suffix)]
84    else:
85        return s[:]

removesuffix()

This is built-in to string functions python3.9 but we use 3.8 for now.

s: String to check suffix: String suffix to look for

Returns: String modified with suffix removed

def resolve_gocd_pipeline_variable(param)
 88def resolve_gocd_pipeline_variable(param):
 89    """
 90    resolve_gocd_pipeline_variable()
 91
 92    GoCD pipeline variables get passed in as a literal string.
 93    Quickly check, resolve and pass back the true environment variable content.
 94    The `literal string` could be a variable surrounded by characters.
 95
 96    Examples:
 97        * $ENV_NAME
 98        * ${ENV_NAME}
 99        * my_${ENV_NAME}
100        * ${ENV_NAME}_rc
101        * hello${ENV_NAME}goodbye
102
103    param: String containing potential env variable
104
105    Returns: String containing resolved env variable or original param if it can't resolve
106    """
107    _param = None
108
109    if "${" in param and "}" in param:
110        if param.startswith("${") and param.endswith("}"):
111            _param = removeprefix(param, "${")
112            _param = removesuffix(_param, "}")
113            _param = os.environ.get(_param, None)
114        else:
115            m = re.search('\\$\\{(.+?)\\}', param)
116            if m:
117                found = m.group(1)
118                _param = os.environ.get(found, None)
119                _param = _param if _param is None else param.replace("${" + found + "}", _param)
120    elif param.startswith("$"):
121        _param = removeprefix(param, "$")
122        _param = os.environ.get(_param, None)
123
124    return _param if _param is not None else param

resolve_gocd_pipeline_variable()

GoCD pipeline variables get passed in as a literal string. Quickly check, resolve and pass back the true environment variable content. The literal string could be a variable surrounded by characters.

Examples: * $ENV_NAME * ${ENV_NAME} * my_${ENV_NAME} * ${ENV_NAME}_rc * hello${ENV_NAME}goodbye

param: String containing potential env variable

Returns: String containing resolved env variable or original param if it can't resolve

def subprocess_long( args: Union[str, List[str]], timeout=None, delay=None, check=None, shell=None, env=None)
127def subprocess_long(args: Union[str, List[str]], timeout=None, delay=None, check=None, shell=None, env=None):
128    """
129    subprocess_long():
130
131    imports subprocess_tee
132
133    Run long-running commands that are wrapped into a continuous loggy.info statement to keep
134    pipelines from exiting on zero output back to the GoCD server.
135
136    * Usage: common.subprocess_long(cmd="make install", timeout=15, delay=5, check=True)
137
138    timeout defaults to 30 minutes
139    delay defaults to 10 seconds
140    check defaults to False, use returned output.returncode to check for failure.
141        (Set to True if you want this function to error on command failure instead)
142    shell defaults to False
143    env defaults to None
144    """
145    timeout = 30 if timeout is None else timeout
146    delay = 10 if delay is None else delay
147    check = False if check is None else check
148    shell = False if shell is None else shell
149
150    if isinstance(args, str):
151        cmd = args
152    else:
153        # run was called with a list instead of a single item but asyncio
154        # create_subprocess_shell requires command as a single string, so
155        # we need to convert it to string
156        cmd = join(args)
157
158    longRunningProcessStart = 'counter=0; while [ $counter -lt ' + \
159        str(timeout*60) + \
160        ' ]; do counter=$((counter+' + \
161        str(delay) + \
162        ')); sleep ' + \
163        str(delay) + \
164        '; echo "common.subprocess_long(): LONG RUNNING PROCESS ENABLED - Running for $counter of ' + \
165        str(timeout*60) + \
166        ' seconds."; done & export _LONG_RUNNING_PROCESS=$!; '
167
168    longRunningProcessStop = '; retcode=$?; kill $_LONG_RUNNING_PROCESS; exit $retcode'
169
170    _process_output = run(longRunningProcessStart + cmd + longRunningProcessStop, env=env, shell=shell, check=check)
171
172    # loggy.info(_process_output.stdout)
173    # loggy.info(_process_output.stderr)
174    # loggy.info(_process_output.returncode)
175    return _process_output

subprocess_long():

imports subprocess_tee

Run long-running commands that are wrapped into a continuous loggy.info statement to keep pipelines from exiting on zero output back to the GoCD server.

  • Usage: common.subprocess_long(cmd="make install", timeout=15, delay=5, check=True)

timeout defaults to 30 minutes delay defaults to 10 seconds check defaults to False, use returned output.returncode to check for failure. (Set to True if you want this function to error on command failure instead) shell defaults to False env defaults to None

def subprocess_run(args: Union[str, List[str]], **kwargs: Any)
178def subprocess_run(args: Union[str, List[str]], **kwargs: Any):
179    """
180    subprocess_run():
181
182    Replace the default subprocess_tee.run with check=True as default
183
184    * Usage: common.subprocess_run("make install")
185    """
186
187    if isinstance(args, str):
188        cmd = args
189    else:
190        # run was called with a list instead of a single item but asyncio
191        # create_subprocess_shell requires command as a single string, so
192        # we need to convert it to string
193        cmd = join(args)
194
195    try:
196        my_kwargs = kwargs.copy()
197        my_kwargs['check'] = kwargs.get("check", True)
198        my_kwargs['shell'] = kwargs.get("shell", True)
199        _process_output = run(cmd, **my_kwargs)
200    except subprocess.CalledProcessError as e:
201        loggy.error(f"common.subprocess_run(): Error: {str(e)}")
202        if _process_output and _process_output.stderr:
203            loggy.error(f"common.subprocess_run(): Process STDERR: {_process_output.stderr}")
204
205        raise
206
207    return _process_output

subprocess_run():

Replace the default subprocess_tee.run with check=True as default

  • Usage: common.subprocess_run("make install")
def remove_empty_from_dict(d)
210def remove_empty_from_dict(d):
211    """
212    remove_empty_from_dict()
213
214    Removes empty values and empty lists from a dictionary
215
216    d: dict to modify
217
218    Returns: dict modified
219    """
220
221    if type(d) is dict:
222        return dict((k, remove_empty_from_dict(v)) for k, v in d.items() if v and remove_empty_from_dict(v))
223    elif type(d) is list:
224        return [remove_empty_from_dict(v) for v in d if v and remove_empty_from_dict(v)]
225    else:
226        return d

remove_empty_from_dict()

Removes empty values and empty lists from a dictionary

d: dict to modify

Returns: dict modified