skilleter-thingy 0.0.40__py3-none-any.whl → 0.0.42__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 skilleter-thingy might be problematic. Click here for more details.
- skilleter_thingy/__init__.py +6 -0
- skilleter_thingy/addpath.py +107 -0
- skilleter_thingy/borger.py +269 -0
- skilleter_thingy/console_colours.py +63 -0
- skilleter_thingy/diskspacecheck.py +67 -0
- skilleter_thingy/docker_purge.py +113 -0
- skilleter_thingy/ffind.py +536 -0
- skilleter_thingy/ggit.py +90 -0
- skilleter_thingy/ggrep.py +154 -0
- skilleter_thingy/git_br.py +180 -0
- skilleter_thingy/git_ca.py +142 -0
- skilleter_thingy/git_cleanup.py +287 -0
- skilleter_thingy/git_co.py +220 -0
- skilleter_thingy/git_common.py +61 -0
- skilleter_thingy/git_hold.py +154 -0
- skilleter_thingy/git_mr.py +92 -0
- skilleter_thingy/git_parent.py +77 -0
- skilleter_thingy/git_review.py +1428 -0
- skilleter_thingy/git_update.py +385 -0
- skilleter_thingy/git_wt.py +96 -0
- skilleter_thingy/gitcmp_helper.py +322 -0
- skilleter_thingy/gitprompt.py +274 -0
- skilleter_thingy/gl.py +174 -0
- skilleter_thingy/gphotosync.py +610 -0
- skilleter_thingy/linecount.py +155 -0
- skilleter_thingy/moviemover.py +133 -0
- skilleter_thingy/photodupe.py +136 -0
- skilleter_thingy/phototidier.py +248 -0
- skilleter_thingy/py_audit.py +131 -0
- skilleter_thingy/readable.py +270 -0
- skilleter_thingy/remdir.py +126 -0
- skilleter_thingy/rmdupe.py +550 -0
- skilleter_thingy/rpylint.py +91 -0
- skilleter_thingy/splitpics.py +99 -0
- skilleter_thingy/strreplace.py +82 -0
- skilleter_thingy/sysmon.py +435 -0
- skilleter_thingy/tfm.py +920 -0
- skilleter_thingy/tfparse.py +101 -0
- skilleter_thingy/thingy/__init__.py +6 -0
- skilleter_thingy/thingy/colour.py +213 -0
- skilleter_thingy/thingy/dc_curses.py +278 -0
- skilleter_thingy/thingy/dc_defaults.py +221 -0
- skilleter_thingy/thingy/dc_util.py +50 -0
- skilleter_thingy/thingy/dircolors.py +308 -0
- skilleter_thingy/thingy/docker.py +95 -0
- skilleter_thingy/thingy/files.py +142 -0
- skilleter_thingy/thingy/git.py +1371 -0
- skilleter_thingy/thingy/git2.py +1307 -0
- skilleter_thingy/thingy/gitlab.py +193 -0
- skilleter_thingy/thingy/logger.py +112 -0
- skilleter_thingy/thingy/path.py +156 -0
- skilleter_thingy/thingy/popup.py +87 -0
- skilleter_thingy/thingy/process.py +112 -0
- skilleter_thingy/thingy/run.py +334 -0
- skilleter_thingy/thingy/tfm_pane.py +595 -0
- skilleter_thingy/thingy/tidy.py +160 -0
- skilleter_thingy/trimpath.py +84 -0
- skilleter_thingy/window_rename.py +92 -0
- skilleter_thingy/xchmod.py +125 -0
- skilleter_thingy/yamlcheck.py +89 -0
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/METADATA +5 -1
- skilleter_thingy-0.0.42.dist-info/RECORD +66 -0
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/entry_points.txt +1 -0
- skilleter_thingy-0.0.42.dist-info/top_level.txt +1 -0
- skilleter_thingy-0.0.40.dist-info/RECORD +0 -6
- skilleter_thingy-0.0.40.dist-info/top_level.txt +0 -1
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/LICENSE +0 -0
- {skilleter_thingy-0.0.40.dist-info → skilleter_thingy-0.0.42.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" GitLab module - implemented using the REST API as some features are not
|
|
5
|
+
available (or don't work) in the official Python module
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2017-20 John Skilleter
|
|
8
|
+
|
|
9
|
+
Licence: GPL v3 or later
|
|
10
|
+
|
|
11
|
+
Note: There are two types of function for returning data from GitLab;
|
|
12
|
+
the paged functions and the non-paged ones - the paged ones return a page
|
|
13
|
+
(normally 20 items) of data and need to be called repeated until no data is
|
|
14
|
+
left whereas the non-paged ones query all the data and concatenate it
|
|
15
|
+
together.
|
|
16
|
+
|
|
17
|
+
The paged functions expect a full request string with the URL, as returned
|
|
18
|
+
by the request_string() member. The non-paged ones call request_string()
|
|
19
|
+
to add the URL & API prefix.
|
|
20
|
+
"""
|
|
21
|
+
################################################################################
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
import os
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import requests
|
|
28
|
+
except ModuleNotFoundError:
|
|
29
|
+
sys.stderr.write('This code requires the Python "requests" module which should be installed via your package manager\n')
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
################################################################################
|
|
33
|
+
|
|
34
|
+
class GitLabError(Exception):
|
|
35
|
+
""" Gitlab exceptions """
|
|
36
|
+
|
|
37
|
+
def __init__(self, response):
|
|
38
|
+
""" Save the error code and text """
|
|
39
|
+
|
|
40
|
+
self.status = response.status_code
|
|
41
|
+
self.message = response.reason
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
""" Return a string version of the exception """
|
|
45
|
+
|
|
46
|
+
return '%s: %s' % (self.status, self.message)
|
|
47
|
+
|
|
48
|
+
################################################################################
|
|
49
|
+
|
|
50
|
+
class GitLab:
|
|
51
|
+
""" Class for GitLab access """
|
|
52
|
+
|
|
53
|
+
def __init__(self, gitlab, token=None):
|
|
54
|
+
""" Initialisation """
|
|
55
|
+
|
|
56
|
+
# Save the GitLab URL
|
|
57
|
+
|
|
58
|
+
self.gitlab = gitlab
|
|
59
|
+
|
|
60
|
+
# If we have a private token use it, otherwise try and get it from
|
|
61
|
+
# the environmnet
|
|
62
|
+
|
|
63
|
+
self.token = token if token else os.getenv('GITLAB_TOKEN', None)
|
|
64
|
+
|
|
65
|
+
# Create the default header for requests
|
|
66
|
+
|
|
67
|
+
self.header = {'Private-Token': self.token}
|
|
68
|
+
|
|
69
|
+
################################################################################
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def encode_project(name):
|
|
73
|
+
""" Encode a project name in the form request by GitLab requests """
|
|
74
|
+
|
|
75
|
+
return name.replace('/', '%2F')
|
|
76
|
+
|
|
77
|
+
################################################################################
|
|
78
|
+
|
|
79
|
+
def request_string(self, request):
|
|
80
|
+
""" Add the URL/API header onto a request string """
|
|
81
|
+
|
|
82
|
+
return '%s/api/v4/%s' % (self.gitlab, request)
|
|
83
|
+
|
|
84
|
+
################################################################################
|
|
85
|
+
|
|
86
|
+
def request(self, request, parameters=None):
|
|
87
|
+
""" Send a request to GitLab - handles pagination and returns all the
|
|
88
|
+
results concatenated together """
|
|
89
|
+
|
|
90
|
+
if parameters:
|
|
91
|
+
request = '%s?%s' % (request, '&'.join(parameters))
|
|
92
|
+
|
|
93
|
+
gl_request = self.request_string(request)
|
|
94
|
+
|
|
95
|
+
# Keep requesting data until there's no 'next' link in the response
|
|
96
|
+
|
|
97
|
+
while True:
|
|
98
|
+
response = requests.get(gl_request, headers=self.header)
|
|
99
|
+
|
|
100
|
+
if not response:
|
|
101
|
+
raise GitLabError(response)
|
|
102
|
+
|
|
103
|
+
yield response.json()
|
|
104
|
+
|
|
105
|
+
if 'next' not in response.links:
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
gl_request = response.links['next']['url']
|
|
109
|
+
|
|
110
|
+
################################################################################
|
|
111
|
+
|
|
112
|
+
def paged_request(self, request):
|
|
113
|
+
""" Send a request to GitLab - returns all the results concatenated together
|
|
114
|
+
and returns a page of results along with the request for the next page of
|
|
115
|
+
results (if any).
|
|
116
|
+
|
|
117
|
+
Note that the request parameter is the full request string as returned by
|
|
118
|
+
request_string(). """
|
|
119
|
+
|
|
120
|
+
response = requests.get(request, headers=self.header)
|
|
121
|
+
|
|
122
|
+
result = response.json()
|
|
123
|
+
|
|
124
|
+
if not response:
|
|
125
|
+
raise GitLabError(response)
|
|
126
|
+
|
|
127
|
+
request = response.links['next']['url'] if 'next' in response.links else None
|
|
128
|
+
|
|
129
|
+
return result, request
|
|
130
|
+
|
|
131
|
+
################################################################################
|
|
132
|
+
|
|
133
|
+
def projects(self):
|
|
134
|
+
""" Return a list of projects """
|
|
135
|
+
|
|
136
|
+
return self.request('projects')
|
|
137
|
+
|
|
138
|
+
################################################################################
|
|
139
|
+
|
|
140
|
+
def branches(self, repo):
|
|
141
|
+
""" Return the list of branches in a repo """
|
|
142
|
+
|
|
143
|
+
for batch in self.request('projects/%s/repository/branches' % self.encode_project(repo)):
|
|
144
|
+
for branch in batch:
|
|
145
|
+
yield branch
|
|
146
|
+
|
|
147
|
+
################################################################################
|
|
148
|
+
|
|
149
|
+
def merge_requests(self, **kwargs):
|
|
150
|
+
""" Return a list of merge requests filtered according to the parameters """
|
|
151
|
+
|
|
152
|
+
request = 'merge_requests'
|
|
153
|
+
|
|
154
|
+
parameters = []
|
|
155
|
+
|
|
156
|
+
for data in kwargs:
|
|
157
|
+
parameters.append('%s=%s' % (data, kwargs[data]))
|
|
158
|
+
|
|
159
|
+
for result in self.request(request, parameters):
|
|
160
|
+
for r in result:
|
|
161
|
+
yield r
|
|
162
|
+
|
|
163
|
+
################################################################################
|
|
164
|
+
|
|
165
|
+
def default_branch(self, repo):
|
|
166
|
+
""" Query gitlab to retreive the default branch for the repo """
|
|
167
|
+
|
|
168
|
+
# Look for the default branch
|
|
169
|
+
|
|
170
|
+
for branch in self.branches(repo):
|
|
171
|
+
if branch['default']:
|
|
172
|
+
return branch['name']
|
|
173
|
+
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
################################################################################
|
|
177
|
+
|
|
178
|
+
def isbranch(self, repo, branchname):
|
|
179
|
+
""" Return True if the branch exists in the repo """
|
|
180
|
+
|
|
181
|
+
request = self.request_string('projects/%s/repository/branches' % self.encode_project(repo))
|
|
182
|
+
|
|
183
|
+
while True:
|
|
184
|
+
branches, request = self.paged_request(request)
|
|
185
|
+
|
|
186
|
+
for branch in branches:
|
|
187
|
+
if branch['name'] == branchname:
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
if not request:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
return False
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Thingy logging functionality - wraps the Pythong logging module
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2017 John Skilleter
|
|
7
|
+
|
|
8
|
+
Licence: GPL v3 or later
|
|
9
|
+
"""
|
|
10
|
+
################################################################################
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
################################################################################
|
|
17
|
+
|
|
18
|
+
CRITICAL = logging.CRITICAL
|
|
19
|
+
ERROR = logging.ERROR
|
|
20
|
+
WARNING = logging.WARNING
|
|
21
|
+
INFO = logging.INFO
|
|
22
|
+
DEBUG = logging.DEBUG
|
|
23
|
+
NOTSET = logging.NOTSET
|
|
24
|
+
|
|
25
|
+
LOG_LEVELS = {
|
|
26
|
+
'CRITICAL': CRITICAL, 'ERROR': ERROR, 'WARNING': WARNING, 'INFO': INFO,
|
|
27
|
+
'DEBUG': DEBUG, 'NOTSET': NOTSET
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
__config_done__ = False
|
|
31
|
+
|
|
32
|
+
################################################################################
|
|
33
|
+
|
|
34
|
+
def set_logging(log, name):
|
|
35
|
+
""" If an environment variable called NAME_DEBUG is set and defines a
|
|
36
|
+
log level that is more verbose than the current level then set
|
|
37
|
+
that level (you can only increase verbosity via the variable, not
|
|
38
|
+
decrease it). """
|
|
39
|
+
|
|
40
|
+
# Check whether there is an environment variable setting the debug level
|
|
41
|
+
|
|
42
|
+
env_name = '%s_DEBUG' % name.upper()
|
|
43
|
+
|
|
44
|
+
value = os.getenv(env_name, None)
|
|
45
|
+
|
|
46
|
+
if value is not None:
|
|
47
|
+
value = value.upper()
|
|
48
|
+
|
|
49
|
+
current = log.getEffectiveLevel()
|
|
50
|
+
|
|
51
|
+
# Check for a textual level in the value and if no match, try
|
|
52
|
+
# for an integer level ignoring invalid values.
|
|
53
|
+
|
|
54
|
+
if value in LOG_LEVELS:
|
|
55
|
+
if current > LOG_LEVELS[value]:
|
|
56
|
+
log.setLevel(LOG_LEVELS[value])
|
|
57
|
+
else:
|
|
58
|
+
try:
|
|
59
|
+
intlevel = int(value)
|
|
60
|
+
|
|
61
|
+
if current > intlevel:
|
|
62
|
+
log.setLevel(intlevel)
|
|
63
|
+
|
|
64
|
+
except ValueError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return log
|
|
68
|
+
|
|
69
|
+
################################################################################
|
|
70
|
+
|
|
71
|
+
def init(name):
|
|
72
|
+
""" Initilise logging and create a logger.
|
|
73
|
+
If the environment variable NAME_DEBUG is set to a value in LOG_LEVELS
|
|
74
|
+
then the log level is set to that level. If NAME_DEBUG is an integer
|
|
75
|
+
then the same applies, otherwise, by default, the log level is CRITICAL """
|
|
76
|
+
|
|
77
|
+
# Create the new logger
|
|
78
|
+
|
|
79
|
+
log = logging.getLogger(name)
|
|
80
|
+
|
|
81
|
+
# Default log level is CRITICAL
|
|
82
|
+
|
|
83
|
+
log.setLevel(CRITICAL)
|
|
84
|
+
|
|
85
|
+
# Set logging according to the value of THINGY_DEBUG (if set) then
|
|
86
|
+
# override with the logger-specific variable (again, if set)
|
|
87
|
+
|
|
88
|
+
set_logging(log, 'THINGY')
|
|
89
|
+
set_logging(log, name)
|
|
90
|
+
|
|
91
|
+
return log
|
|
92
|
+
|
|
93
|
+
################################################################################
|
|
94
|
+
# Entry point
|
|
95
|
+
|
|
96
|
+
# Ensure that the logging module is initialise
|
|
97
|
+
|
|
98
|
+
if not __config_done__:
|
|
99
|
+
logging.basicConfig()
|
|
100
|
+
__config_done__ = True
|
|
101
|
+
|
|
102
|
+
if __name__ == '__main__':
|
|
103
|
+
demo = init('wombat')
|
|
104
|
+
|
|
105
|
+
demo.critical('Critical error')
|
|
106
|
+
|
|
107
|
+
# These messages should only appear if the WOMBAT_DEBUG environment variable
|
|
108
|
+
# is set to an appropriate value (ERROR, WARNING or INFO)
|
|
109
|
+
|
|
110
|
+
demo.error('Error message')
|
|
111
|
+
demo.warning('Warning message')
|
|
112
|
+
demo.info('Info message')
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Thingy file and directory functionality
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2017-18 John Skilleter
|
|
7
|
+
|
|
8
|
+
Licence: GPL v3 or later
|
|
9
|
+
"""
|
|
10
|
+
################################################################################
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
import thingy.logger as logger
|
|
15
|
+
|
|
16
|
+
################################################################################
|
|
17
|
+
|
|
18
|
+
class PathError(Exception):
|
|
19
|
+
""" Exception raised by the module """
|
|
20
|
+
|
|
21
|
+
def __init__(self, msg):
|
|
22
|
+
super(PathError, self).__init__(msg)
|
|
23
|
+
self.msg = msg
|
|
24
|
+
|
|
25
|
+
################################################################################
|
|
26
|
+
|
|
27
|
+
def is_subdirectory(root_path, sub_path):
|
|
28
|
+
""" Return True if sub_path is a sub-directory of root_path """
|
|
29
|
+
|
|
30
|
+
abs_sub_path = os.path.abspath(sub_path)
|
|
31
|
+
abs_root_path = os.path.abspath(root_path)
|
|
32
|
+
|
|
33
|
+
log.debug('root path: %s', abs_root_path)
|
|
34
|
+
log.debug('sub path : %s', abs_sub_path)
|
|
35
|
+
|
|
36
|
+
return abs_sub_path.startswith('%s/' % abs_root_path)
|
|
37
|
+
|
|
38
|
+
################################################################################
|
|
39
|
+
|
|
40
|
+
def trimpath(full_path, trim_width):
|
|
41
|
+
""" Trim a path to a specified maximum width, but always leaving the
|
|
42
|
+
lowest-level directory (even if it exceeds the trim width). """
|
|
43
|
+
|
|
44
|
+
log.debug('Path: "%s"', full_path)
|
|
45
|
+
log.debug('Required width: %d', trim_width)
|
|
46
|
+
|
|
47
|
+
full_path = os.path.abspath(full_path)
|
|
48
|
+
|
|
49
|
+
# Remove any trailing '/' from the path
|
|
50
|
+
|
|
51
|
+
if full_path != '/' and full_path[-1] == '/':
|
|
52
|
+
full_path = full_path[:-1]
|
|
53
|
+
|
|
54
|
+
# If the path starts with the user's home directory then convert the prefix
|
|
55
|
+
# into a '~'
|
|
56
|
+
|
|
57
|
+
home_dir = os.path.expanduser('~')
|
|
58
|
+
|
|
59
|
+
if full_path == home_dir:
|
|
60
|
+
full_path = '~'
|
|
61
|
+
log.debug('Converted path to "~"')
|
|
62
|
+
|
|
63
|
+
elif is_subdirectory(home_dir, full_path):
|
|
64
|
+
full_path = "~/%s" % full_path[len(home_dir) + 1:]
|
|
65
|
+
|
|
66
|
+
log.debug('Converted path to "%s"', full_path)
|
|
67
|
+
|
|
68
|
+
# If the path is too long then slice it into directories and cut sub-directories
|
|
69
|
+
# out of the middle until it is short enough. Always leave the last element
|
|
70
|
+
# in place, even if this means total length exceeds the requirement.
|
|
71
|
+
|
|
72
|
+
path_len = len(full_path)
|
|
73
|
+
|
|
74
|
+
log.debug('Path length: %d', path_len)
|
|
75
|
+
|
|
76
|
+
# Already within maximum width, so just return it
|
|
77
|
+
|
|
78
|
+
if path_len <= trim_width:
|
|
79
|
+
return full_path
|
|
80
|
+
|
|
81
|
+
# Split into an array of directories and trim out middle ones
|
|
82
|
+
|
|
83
|
+
directories = full_path.split('/')
|
|
84
|
+
|
|
85
|
+
log.debug('Path has %d elements: "%s"', len(directories), directories)
|
|
86
|
+
|
|
87
|
+
if len(directories) == 1:
|
|
88
|
+
# If there's only one element in the path, just give up
|
|
89
|
+
|
|
90
|
+
log.debug('Only 1 directory in the path, leaving it as-is')
|
|
91
|
+
|
|
92
|
+
elif len(directories) == 2:
|
|
93
|
+
# If there's only two elements in the path then replace the first
|
|
94
|
+
# element with '...' and give up
|
|
95
|
+
|
|
96
|
+
log.debug('Only 2 directories in the path, so setting the first to "..."')
|
|
97
|
+
|
|
98
|
+
directories[0] = '...'
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
# Start in the middle and remove entries to the left and right until the total
|
|
102
|
+
# path length is shortened to a sufficient extent
|
|
103
|
+
|
|
104
|
+
right = len(directories) // 2
|
|
105
|
+
left = right - 1
|
|
106
|
+
first = True
|
|
107
|
+
|
|
108
|
+
while path_len > trim_width:
|
|
109
|
+
|
|
110
|
+
path_len -= len(directories[right]) + 1
|
|
111
|
+
|
|
112
|
+
if first:
|
|
113
|
+
path_len += 4
|
|
114
|
+
first = False
|
|
115
|
+
|
|
116
|
+
if right == len(directories) - 1:
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
right += 1
|
|
120
|
+
|
|
121
|
+
if path_len > trim_width:
|
|
122
|
+
path_len -= len(directories[left]) + 1
|
|
123
|
+
|
|
124
|
+
if left == 0:
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
left -= 1
|
|
128
|
+
|
|
129
|
+
log.debug('Removing entries %d..%d from the path', left, right)
|
|
130
|
+
|
|
131
|
+
directories = directories[0:left + 1] + ['...'] + directories[right:]
|
|
132
|
+
|
|
133
|
+
full_path = '/'.join(directories)
|
|
134
|
+
|
|
135
|
+
log.debug('Calculated width is %d and actual width is %d', path_len, len(full_path))
|
|
136
|
+
|
|
137
|
+
return full_path
|
|
138
|
+
|
|
139
|
+
################################################################################
|
|
140
|
+
|
|
141
|
+
log = logger.init('tgy_path')
|
|
142
|
+
|
|
143
|
+
if __name__ == '__main__':
|
|
144
|
+
PARENT = '/1/2/3/5'
|
|
145
|
+
CHILD = '/1/2/3/5/6'
|
|
146
|
+
|
|
147
|
+
print('Is %s a subdirectory of %s: %s (expecting True)' % (CHILD, PARENT, is_subdirectory(PARENT, CHILD)))
|
|
148
|
+
print('Is %s a subdirectory of %s: %s (expecting False)' % (PARENT, CHILD, is_subdirectory(CHILD, PARENT)))
|
|
149
|
+
|
|
150
|
+
LONG_PATH = '/home/jms/source/womble-biscuit-token-generation-service/subdirectory'
|
|
151
|
+
|
|
152
|
+
for pathname in (LONG_PATH, os.path.realpath('.')):
|
|
153
|
+
print('Full path: %s' % pathname)
|
|
154
|
+
|
|
155
|
+
for length in (80, 60, 40, 20, 16, 10):
|
|
156
|
+
print('Trimmed to %d characters: %s' % (length, trimpath(pathname, length)))
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
""" Curses-based pop-up message
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
with PopUp(curses_screen, message, colour):
|
|
6
|
+
do_stuff
|
|
7
|
+
|
|
8
|
+
Popup message is displayed for the duration of the with statement, and
|
|
9
|
+
has optional parameters to wait for a keypress, and/or pause before removing
|
|
10
|
+
the popup again.
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
################################################################################
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
import curses
|
|
17
|
+
|
|
18
|
+
################################################################################
|
|
19
|
+
|
|
20
|
+
class PopUp():
|
|
21
|
+
""" Class to enable popup windows to be used via with statements """
|
|
22
|
+
|
|
23
|
+
def __init__(self, screen, msg, colour, waitkey=False, sleep=True, centre=True, refresh=True):
|
|
24
|
+
""" Initialisation - just save the popup parameters """
|
|
25
|
+
|
|
26
|
+
self.panel = None
|
|
27
|
+
self.screen = screen
|
|
28
|
+
self.msg = msg
|
|
29
|
+
self.centre = centre
|
|
30
|
+
self.colour = curses.color_pair(colour)
|
|
31
|
+
self.refresh = refresh
|
|
32
|
+
self.sleep = sleep and not waitkey
|
|
33
|
+
self.waitkey = waitkey
|
|
34
|
+
self.start_time = 0
|
|
35
|
+
|
|
36
|
+
def __enter__(self):
|
|
37
|
+
""" Display the popup """
|
|
38
|
+
|
|
39
|
+
lines = self.msg.split('\n')
|
|
40
|
+
height = len(lines)
|
|
41
|
+
|
|
42
|
+
width = 0
|
|
43
|
+
for line in lines:
|
|
44
|
+
width = max(width, len(line))
|
|
45
|
+
|
|
46
|
+
width += 2
|
|
47
|
+
height += 2
|
|
48
|
+
|
|
49
|
+
size_y, size_x = self.screen.getmaxyx()
|
|
50
|
+
|
|
51
|
+
window = curses.newwin(height, width, (size_y - height) // 2, (size_x - width) // 2)
|
|
52
|
+
self.panel = curses.panel.new_panel(window)
|
|
53
|
+
|
|
54
|
+
window.bkgd(' ', self.colour)
|
|
55
|
+
for y_pos, line in enumerate(lines):
|
|
56
|
+
x_pos = (width - len(line)) // 2 if self.centre else 1
|
|
57
|
+
window.addstr(y_pos + 1, x_pos, line, self.colour)
|
|
58
|
+
|
|
59
|
+
self.panel.top()
|
|
60
|
+
curses.panel.update_panels()
|
|
61
|
+
self.screen.refresh()
|
|
62
|
+
|
|
63
|
+
self.start_time = time.monotonic()
|
|
64
|
+
|
|
65
|
+
if self.waitkey:
|
|
66
|
+
while True:
|
|
67
|
+
keypress = self.screen.getch()
|
|
68
|
+
if keypress != curses.KEY_RESIZE:
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
curses.panel.update_panels()
|
|
72
|
+
self.screen.refresh()
|
|
73
|
+
|
|
74
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
75
|
+
""" Remove the popup """
|
|
76
|
+
|
|
77
|
+
if self.panel:
|
|
78
|
+
if self.sleep:
|
|
79
|
+
elapsed = time.monotonic() - self.start_time
|
|
80
|
+
|
|
81
|
+
if elapsed < 1:
|
|
82
|
+
time.sleep(1 - elapsed)
|
|
83
|
+
|
|
84
|
+
del self.panel
|
|
85
|
+
|
|
86
|
+
if self.refresh:
|
|
87
|
+
self.screen.refresh()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
################################################################################
|
|
4
|
+
""" Basic subprocess handling - simplified API to the Python subprocess module
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2017-18 John Skilleter
|
|
7
|
+
|
|
8
|
+
Licence: GPL v3 or later
|
|
9
|
+
"""
|
|
10
|
+
################################################################################
|
|
11
|
+
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import thingy.logger as logger
|
|
16
|
+
|
|
17
|
+
################################################################################
|
|
18
|
+
|
|
19
|
+
class RunError(Exception):
|
|
20
|
+
""" Run exception """
|
|
21
|
+
|
|
22
|
+
def __init__(self, msg, status=1):
|
|
23
|
+
super(RunError, self).__init__(msg)
|
|
24
|
+
self.msg = msg
|
|
25
|
+
self.status = status
|
|
26
|
+
|
|
27
|
+
################################################################################
|
|
28
|
+
|
|
29
|
+
def run(command, foreground=False, shell=False):
|
|
30
|
+
""" Run the specified command and return the output.
|
|
31
|
+
command - the command to run as an array of command+arguments
|
|
32
|
+
foreground - set to True to run the command in the foreground, using stdin/err/out
|
|
33
|
+
shell - set to True to run the command inside a shell (allows the command to be specified
|
|
34
|
+
as a string, but needs spaces to bew quoted
|
|
35
|
+
Returns an empty array if foreground is True or an array of the output otherwise. """
|
|
36
|
+
|
|
37
|
+
# TODO: for background use subprocess.Popen but use devnull = open('/dev/null', 'w') for stdio and return proc instead of communicating with it?
|
|
38
|
+
|
|
39
|
+
log.info('Running "%s"', ' '.join(command))
|
|
40
|
+
|
|
41
|
+
# If running in the foreground, run the command and either return an empty value
|
|
42
|
+
# on success (output is to the console) or raise a RunError
|
|
43
|
+
|
|
44
|
+
if foreground:
|
|
45
|
+
try:
|
|
46
|
+
if shell:
|
|
47
|
+
# TODO: Handle command lines with parameters containing spaces
|
|
48
|
+
command = ' '.join(command)
|
|
49
|
+
|
|
50
|
+
status = subprocess.run(command, shell=shell).returncode
|
|
51
|
+
if status:
|
|
52
|
+
raise RunError('Error %d' % status, status)
|
|
53
|
+
else:
|
|
54
|
+
return []
|
|
55
|
+
except OSError as exc:
|
|
56
|
+
raise RunError(exc)
|
|
57
|
+
else:
|
|
58
|
+
# Run the command and capture stdout and stderr
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
proc = subprocess.run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE, universal_newlines=True)
|
|
62
|
+
except OSError as exc:
|
|
63
|
+
raise RunError(exc)
|
|
64
|
+
|
|
65
|
+
log.info('Stdout: %s', proc.stdout)
|
|
66
|
+
log.info('Stderr: %s', proc.stderr)
|
|
67
|
+
|
|
68
|
+
# If it returned an error raise a RunError exception with the stdout text as the
|
|
69
|
+
# exception message
|
|
70
|
+
|
|
71
|
+
if proc.returncode:
|
|
72
|
+
raise RunError(proc.stderr)
|
|
73
|
+
|
|
74
|
+
# Otherwise return the stdout data or nothing
|
|
75
|
+
|
|
76
|
+
if proc.stdout:
|
|
77
|
+
output = proc.stdout.split('\n')
|
|
78
|
+
else:
|
|
79
|
+
output = []
|
|
80
|
+
|
|
81
|
+
# Remove trailing blank lines from the output
|
|
82
|
+
|
|
83
|
+
while output and output[-1] == '':
|
|
84
|
+
output = output[:-1]
|
|
85
|
+
|
|
86
|
+
log.info('Output: %s', output)
|
|
87
|
+
return output
|
|
88
|
+
|
|
89
|
+
################################################################################
|
|
90
|
+
|
|
91
|
+
log = logger.init('tgy_process')
|
|
92
|
+
|
|
93
|
+
if __name__ == '__main__':
|
|
94
|
+
print('Run ls -l:')
|
|
95
|
+
|
|
96
|
+
cmd_output = run(['ls', '-l'])
|
|
97
|
+
|
|
98
|
+
for o in cmd_output:
|
|
99
|
+
print(o)
|
|
100
|
+
|
|
101
|
+
print('Run wombat (should fail):')
|
|
102
|
+
try:
|
|
103
|
+
run(['wombat'])
|
|
104
|
+
except RunError as exc:
|
|
105
|
+
print('Failed with error: %s' % exc.msg)
|
|
106
|
+
|
|
107
|
+
if sys.stdout.isatty():
|
|
108
|
+
print('Run vi in the foreground')
|
|
109
|
+
|
|
110
|
+
run(['vi'], foreground=True)
|
|
111
|
+
else:
|
|
112
|
+
print('Not testing call to run() with foreground=True as stdout is not a TTY')
|