skilleter-modules 0.0.1__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.
@@ -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,153 @@
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
+ import logging
14
+
15
+ ################################################################################
16
+
17
+ class PathError(Exception):
18
+ """ Exception raised by the module """
19
+
20
+ def __init__(self, msg):
21
+ super(PathError, self).__init__(msg)
22
+ self.msg = msg
23
+
24
+ ################################################################################
25
+
26
+ def is_subdirectory(root_path, sub_path):
27
+ """ Return True if sub_path is a sub-directory of root_path """
28
+
29
+ abs_sub_path = os.path.abspath(sub_path)
30
+ abs_root_path = os.path.abspath(root_path)
31
+
32
+ logging.debug('root path: %s', abs_root_path)
33
+ logging.debug('sub path : %s', abs_sub_path)
34
+
35
+ return abs_sub_path.startswith('%s/' % abs_root_path)
36
+
37
+ ################################################################################
38
+
39
+ def trimpath(full_path, trim_width):
40
+ """ Trim a path to a specified maximum width, but always leaving the
41
+ lowest-level directory (even if it exceeds the trim width). """
42
+
43
+ logging.debug('Path: "%s"', full_path)
44
+ logging.debug('Required width: %d', trim_width)
45
+
46
+ full_path = os.path.abspath(full_path)
47
+
48
+ # Remove any trailing '/' from the path
49
+
50
+ if full_path != '/' and full_path[-1] == '/':
51
+ full_path = full_path[:-1]
52
+
53
+ # If the path starts with the user's home directory then convert the prefix
54
+ # into a '~'
55
+
56
+ home_dir = os.path.expanduser('~')
57
+
58
+ if full_path == home_dir:
59
+ full_path = '~'
60
+ logging.debug('Converted path to "~"')
61
+
62
+ elif is_subdirectory(home_dir, full_path):
63
+ full_path = "~/%s" % full_path[len(home_dir) + 1:]
64
+
65
+ logging.debug('Converted path to "%s"', full_path)
66
+
67
+ # If the path is too long then slice it into directories and cut sub-directories
68
+ # out of the middle until it is short enough. Always leave the last element
69
+ # in place, even if this means total length exceeds the requirement.
70
+
71
+ path_len = len(full_path)
72
+
73
+ logging.debug('Path length: %d', path_len)
74
+
75
+ # Already within maximum width, so just return it
76
+
77
+ if path_len <= trim_width:
78
+ return full_path
79
+
80
+ # Split into an array of directories and trim out middle ones
81
+
82
+ directories = full_path.split('/')
83
+
84
+ logging.debug('Path has %d elements: "%s"', len(directories), directories)
85
+
86
+ if len(directories) == 1:
87
+ # If there's only one element in the path, just give up
88
+
89
+ logging.debug('Only 1 directory in the path, leaving it as-is')
90
+
91
+ elif len(directories) == 2:
92
+ # If there's only two elements in the path then replace the first
93
+ # element with '...' and give up
94
+
95
+ logging.debug('Only 2 directories in the path, so setting the first to "..."')
96
+
97
+ directories[0] = '...'
98
+
99
+ else:
100
+ # Start in the middle and remove entries to the left and right until the total
101
+ # path length is shortened to a sufficient extent
102
+
103
+ right = len(directories) // 2
104
+ left = right - 1
105
+ first = True
106
+
107
+ while path_len > trim_width:
108
+
109
+ path_len -= len(directories[right]) + 1
110
+
111
+ if first:
112
+ path_len += 4
113
+ first = False
114
+
115
+ if right == len(directories) - 1:
116
+ break
117
+
118
+ right += 1
119
+
120
+ if path_len > trim_width:
121
+ path_len -= len(directories[left]) + 1
122
+
123
+ if left == 0:
124
+ break
125
+
126
+ left -= 1
127
+
128
+ logging.debug('Removing entries %d..%d from the path', left, right)
129
+
130
+ directories = directories[0:left + 1] + ['...'] + directories[right:]
131
+
132
+ full_path = '/'.join(directories)
133
+
134
+ logging.debug('Calculated width is %d and actual width is %d', path_len, len(full_path))
135
+
136
+ return full_path
137
+
138
+ ################################################################################
139
+
140
+ if __name__ == '__main__':
141
+ PARENT = '/1/2/3/5'
142
+ CHILD = '/1/2/3/5/6'
143
+
144
+ print('Is %s a subdirectory of %s: %s (expecting True)' % (CHILD, PARENT, is_subdirectory(PARENT, CHILD)))
145
+ print('Is %s a subdirectory of %s: %s (expecting False)' % (PARENT, CHILD, is_subdirectory(CHILD, PARENT)))
146
+
147
+ LONG_PATH = '/home/jms/source/womble-biscuit-token-generation-service/subdirectory'
148
+
149
+ for pathname in (LONG_PATH, os.path.realpath('.')):
150
+ print('Full path: %s' % pathname)
151
+
152
+ for length in (80, 60, 40, 20, 16, 10):
153
+ print('Trimmed to %d characters: %s' % (length, trimpath(pathname, length)))
@@ -0,0 +1,88 @@
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
+ import curses.panel
18
+
19
+ ################################################################################
20
+
21
+ class PopUp():
22
+ """ Class to enable popup windows to be used via with statements """
23
+
24
+ def __init__(self, screen, msg, colour, waitkey=False, sleep=True, centre=True, refresh=True):
25
+ """ Initialisation - just save the popup parameters """
26
+
27
+ self.panel = None
28
+ self.screen = screen
29
+ self.msg = msg
30
+ self.centre = centre
31
+ self.colour = curses.color_pair(colour)
32
+ self.refresh = refresh
33
+ self.sleep = sleep and not waitkey
34
+ self.waitkey = waitkey
35
+ self.start_time = 0
36
+
37
+ def __enter__(self):
38
+ """ Display the popup """
39
+
40
+ lines = self.msg.split('\n')
41
+ height = len(lines)
42
+
43
+ width = 0
44
+ for line in lines:
45
+ width = max(width, len(line))
46
+
47
+ width += 2
48
+ height += 2
49
+
50
+ size_y, size_x = self.screen.getmaxyx()
51
+
52
+ window = curses.newwin(height, width, (size_y - height) // 2, (size_x - width) // 2)
53
+ self.panel = curses.panel.new_panel(window)
54
+
55
+ window.bkgd(' ', self.colour)
56
+ for y_pos, line in enumerate(lines):
57
+ x_pos = (width - len(line)) // 2 if self.centre else 1
58
+ window.addstr(y_pos + 1, x_pos, line, self.colour)
59
+
60
+ self.panel.top()
61
+ curses.panel.update_panels()
62
+ self.screen.refresh()
63
+
64
+ self.start_time = time.monotonic()
65
+
66
+ if self.waitkey:
67
+ while True:
68
+ keypress = self.screen.getch()
69
+ if keypress != curses.KEY_RESIZE:
70
+ break
71
+
72
+ curses.panel.update_panels()
73
+ self.screen.refresh()
74
+
75
+ def __exit__(self, _exc_type, _exc_value, _exc_traceback):
76
+ """ Remove the popup """
77
+
78
+ if self.panel:
79
+ if self.sleep:
80
+ elapsed = time.monotonic() - self.start_time
81
+
82
+ if elapsed < 1:
83
+ time.sleep(1 - elapsed)
84
+
85
+ del self.panel
86
+
87
+ if self.refresh:
88
+ self.screen.refresh()