damage 0.3.14__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.
- damage/__init__.py +444 -0
- damage/console/damage_cmd.py +126 -0
- damage/gui/assets/DamageAppIcon.icns +0 -0
- damage/gui/assets/DamageAppIcon.ico +0 -0
- damage/gui/assets/DamageAppIcon.jpg +0 -0
- damage/gui/assets/DamageAppIcon.png +0 -0
- damage/gui/assets/LICENCE.txt +10 -0
- damage/gui/damage_gui.py +678 -0
- damage-0.3.14.dist-info/LICENCE.md +9 -0
- damage-0.3.14.dist-info/METADATA +41 -0
- damage-0.3.14.dist-info/RECORD +13 -0
- damage-0.3.14.dist-info/WHEEL +4 -0
- damage-0.3.14.dist-info/entry_points.txt +7 -0
damage/__init__.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Manifest generator for data files.
|
|
3
|
+
|
|
4
|
+
Produces a text file with user specificied checksums for all files
|
|
5
|
+
from the top of a specified tree and checks line length
|
|
6
|
+
and ASCII character status for text files.
|
|
7
|
+
|
|
8
|
+
For statistics program files:
|
|
9
|
+
SAS .sas7bdat
|
|
10
|
+
SPSS .sav
|
|
11
|
+
Stata .dta
|
|
12
|
+
|
|
13
|
+
Checker() will report number of cases and variables as
|
|
14
|
+
rows and columns respectively.
|
|
15
|
+
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
import copy
|
|
19
|
+
import csv
|
|
20
|
+
import hashlib
|
|
21
|
+
import io
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import mimetypes
|
|
25
|
+
import pathlib
|
|
26
|
+
import string
|
|
27
|
+
|
|
28
|
+
import chardet
|
|
29
|
+
import pyreadstat
|
|
30
|
+
|
|
31
|
+
LOGGER = logging.getLogger()
|
|
32
|
+
|
|
33
|
+
VERSION = (0, 3, 14)
|
|
34
|
+
__version__ = '.'.join([str(x) for x in VERSION])
|
|
35
|
+
|
|
36
|
+
#PDB note check private variables with self._Checker__private_var
|
|
37
|
+
#Note *single* underscore before Checker
|
|
38
|
+
class Checker():
|
|
39
|
+
'''
|
|
40
|
+
A collection of various tools attached to a file
|
|
41
|
+
'''
|
|
42
|
+
|
|
43
|
+
def __init__(self, fname: str) -> None: #DONE
|
|
44
|
+
'''
|
|
45
|
+
Initializes Checker instance
|
|
46
|
+
|
|
47
|
+
fname : str
|
|
48
|
+
Path to file
|
|
49
|
+
'''
|
|
50
|
+
#Commercial stats files extensions
|
|
51
|
+
#I am aware that extension checking is not perfect
|
|
52
|
+
self.statfiles = ['.dta', '.sav', '.sas7bdat']
|
|
53
|
+
#brute force is best force
|
|
54
|
+
self.textfiles= ['.dat', '.txt', '.md', '.csv',
|
|
55
|
+
'.tsv', '.asc', '.html', '.xml',
|
|
56
|
+
'.xsd', '.htm', '.log', '.nfo',
|
|
57
|
+
'.text', '.xsl', '.py', '.r',
|
|
58
|
+
'.toml', '.yaml', '.yml']
|
|
59
|
+
self.fname = pathlib.Path(fname)
|
|
60
|
+
#self._ext = fname.suffix
|
|
61
|
+
self.__istext = self.__istextfile()
|
|
62
|
+
self.__text_obj = None
|
|
63
|
+
with open(self.fname, 'rb') as fil:
|
|
64
|
+
self.__fobj_bin = io.BytesIO(fil.read())
|
|
65
|
+
self.encoding = self.__encoding()
|
|
66
|
+
if self.__istext:
|
|
67
|
+
with open(self.fname, encoding=self.encoding.get('encoding')) as f:
|
|
68
|
+
self.__text_obj = io.StringIO(f.read())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def hidden(self)->bool:
|
|
73
|
+
'''
|
|
74
|
+
Returns True if file is hidden (ie, startswith '.')
|
|
75
|
+
or is in in a hidden directory (ie, any directory on the path
|
|
76
|
+
starts with '.')
|
|
77
|
+
'''
|
|
78
|
+
if any([x.startswith('.') for x in self.fname.parts]):
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def __istextfile(self):
|
|
83
|
+
'''
|
|
84
|
+
Check to see if file is a text file based on mimetype.
|
|
85
|
+
Works with extensions only which is not ideal
|
|
86
|
+
'''
|
|
87
|
+
try:
|
|
88
|
+
if ('text' in mimetypes.guess_file_type(self.fname)
|
|
89
|
+
or self.fname.suffix.lower() in self.textfiles):
|
|
90
|
+
return True
|
|
91
|
+
except AttributeError: #soft deprecation fix
|
|
92
|
+
if ('text' in mimetypes.guess_type(self.fname)
|
|
93
|
+
or self.fname.suffix.lower() in self.textfiles):
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def __encoding(self) -> dict: #DONE
|
|
99
|
+
'''
|
|
100
|
+
Returns most likely encoding of self.fname, dict with keys
|
|
101
|
+
encoding, confidence, language (the output of chardet.detect)
|
|
102
|
+
and sets Checker.__is_text
|
|
103
|
+
'''
|
|
104
|
+
enc = chardet.detect(self.__fobj_bin.read())
|
|
105
|
+
self.__fobj_bin.seek(0) #leave it as you found it
|
|
106
|
+
if self.__istext:
|
|
107
|
+
return enc
|
|
108
|
+
|
|
109
|
+
return {'encoding': None,
|
|
110
|
+
'confidence': 0.0,
|
|
111
|
+
'language' : ''}
|
|
112
|
+
|
|
113
|
+
def __del__(self) -> None:#DONE
|
|
114
|
+
'''
|
|
115
|
+
Destructor closes file
|
|
116
|
+
'''
|
|
117
|
+
self.__fobj_bin.close()
|
|
118
|
+
|
|
119
|
+
def produce_digest(self, prot: str = 'md5', blocksize: int = 2*16) -> str: #DONE
|
|
120
|
+
'''
|
|
121
|
+
Returns hex digest for object
|
|
122
|
+
|
|
123
|
+
fname : str
|
|
124
|
+
Path to a file object
|
|
125
|
+
|
|
126
|
+
prot : str
|
|
127
|
+
Hash type. Supported hashes: 'sha1', 'sha224', 'sha256',
|
|
128
|
+
'sha384', 'sha512', 'blake2b', 'blake2s', 'md5'.
|
|
129
|
+
Default: 'md5'
|
|
130
|
+
|
|
131
|
+
blocksize : int
|
|
132
|
+
Read block size in bytes
|
|
133
|
+
'''
|
|
134
|
+
ok_hash = {'sha1' : hashlib.sha1(),
|
|
135
|
+
'sha224' : hashlib.sha224(),
|
|
136
|
+
'sha256' : hashlib.sha256(),
|
|
137
|
+
'sha384' : hashlib.sha384(),
|
|
138
|
+
'sha512' : hashlib.sha512(),
|
|
139
|
+
'blake2b' : hashlib.blake2b(),
|
|
140
|
+
'blake2s' : hashlib.blake2s(),
|
|
141
|
+
'md5': hashlib.md5()}
|
|
142
|
+
|
|
143
|
+
self.__fobj_bin.seek(0)
|
|
144
|
+
try:
|
|
145
|
+
_hash = ok_hash[prot]
|
|
146
|
+
except (UnboundLocalError, KeyError):
|
|
147
|
+
message = ('Unsupported hash type. Valid values are '
|
|
148
|
+
f'{list(ok_hash)}.')
|
|
149
|
+
LOGGER.exception('Unsupported hash type. Valid values are %s', message)
|
|
150
|
+
raise
|
|
151
|
+
|
|
152
|
+
fblock = self.__fobj_bin.read(blocksize)
|
|
153
|
+
while fblock:
|
|
154
|
+
_hash.update(fblock)
|
|
155
|
+
fblock = self.__fobj_bin.read(blocksize)
|
|
156
|
+
return _hash.hexdigest()
|
|
157
|
+
|
|
158
|
+
def flat_tester(self, **kwargs) -> dict: #DONE
|
|
159
|
+
'''
|
|
160
|
+
Checks file for line length and number of records.
|
|
161
|
+
|
|
162
|
+
Returns a dictionary:
|
|
163
|
+
|
|
164
|
+
`{'min_cols': int, 'max_cols' : int, 'numrec':int, 'constant' : bool}`
|
|
165
|
+
'''
|
|
166
|
+
if not kwargs.get('flatfile'):
|
|
167
|
+
return {'min_cols': 'N/A', 'max_cols': 'N/A', 'numrec' : 'N/A',
|
|
168
|
+
'constant': 'N/A', 'encoding' : 'N/A'}
|
|
169
|
+
|
|
170
|
+
if self.fname.suffix.lower() in self.statfiles:
|
|
171
|
+
return self._flat_tester_commercial(**kwargs)
|
|
172
|
+
|
|
173
|
+
if self.__istext:
|
|
174
|
+
return self._flat_tester_txt()
|
|
175
|
+
#this should not happen but you never know
|
|
176
|
+
return {'min_cols': 'N/A', 'max_cols': 'N/A', 'numrec' : 'N/A',
|
|
177
|
+
'constant': 'N/A', 'encoding' : 'N/A'}
|
|
178
|
+
|
|
179
|
+
def _flat_tester_commercial(self, **kwargs) -> dict: #DONE
|
|
180
|
+
'''
|
|
181
|
+
Checks SPSS sav, SAS sas7bdat and Stata .dta files for rectangularity
|
|
182
|
+
|
|
183
|
+
Returns a dictionary:
|
|
184
|
+
|
|
185
|
+
`{'min_cols': int, 'max_cols': int, 'numrec' : int,
|
|
186
|
+
'constant': True, 'encoding': str}`
|
|
187
|
+
|
|
188
|
+
These files are by definition rectanglar, at least as checked here
|
|
189
|
+
by pyreadstat/pandas, so constant will always == True.
|
|
190
|
+
'''
|
|
191
|
+
if not kwargs.get('flatfile'):
|
|
192
|
+
return {'min_cols': 'N/A', 'max_cols': 'N/A', 'numrec' : 'N/A',
|
|
193
|
+
'constant': 'N/A', 'encoding': 'N/A'}
|
|
194
|
+
options = {'.sav' : pyreadstat.read_sav,
|
|
195
|
+
'.dta' : pyreadstat.read_dta,
|
|
196
|
+
'.sas7bdat' : pyreadstat.read_sas7bdat}
|
|
197
|
+
meta = options[self.fname.suffix.lower()](self.fname)[1]
|
|
198
|
+
#self._encoding = meta.file_encoding
|
|
199
|
+
self.encoding['encoding'] = meta.file_encoding
|
|
200
|
+
return {'min_cols':meta.number_columns,
|
|
201
|
+
'max_cols':meta.number_columns,
|
|
202
|
+
'numrec': meta.number_rows,
|
|
203
|
+
'constant':True,
|
|
204
|
+
'encoding': self.encoding['encoding']}
|
|
205
|
+
|
|
206
|
+
def _flat_tester_txt(self) -> dict: #DONE
|
|
207
|
+
'''
|
|
208
|
+
Checks file for line length and number of records.
|
|
209
|
+
|
|
210
|
+
Returns a dictionary:
|
|
211
|
+
|
|
212
|
+
`{'min_cols': int, 'max_cols' : int, 'numrec':int, 'constant' : bool}`
|
|
213
|
+
'''
|
|
214
|
+
linecount = 0
|
|
215
|
+
self.__text_obj.seek(0)
|
|
216
|
+
if not self.__istext:
|
|
217
|
+
raise TypeError('Not a text file')
|
|
218
|
+
maxline = len(self.__text_obj.readline())
|
|
219
|
+
minline = maxline
|
|
220
|
+
orig = maxline # baseline to which new values are compared
|
|
221
|
+
for row in self.__text_obj.readlines():
|
|
222
|
+
linecount += 1
|
|
223
|
+
maxline = max(maxline, len(row))
|
|
224
|
+
minline = min(minline, len(row))
|
|
225
|
+
constant = bool(maxline == orig == minline)
|
|
226
|
+
self.__text_obj.seek(0)
|
|
227
|
+
return {'min_cols': minline, 'max_cols': maxline, 'numrec' : linecount,
|
|
228
|
+
'constant': constant, 'encoding': self.encoding['encoding']}
|
|
229
|
+
|
|
230
|
+
def non_ascii_tester(self, **kwargs) -> list: #DONE
|
|
231
|
+
'''
|
|
232
|
+
Returns a list of dicts of positions of non-ASCII characters in a text file.
|
|
233
|
+
|
|
234
|
+
`[{'row': int, 'col':int, 'char':str}...]`
|
|
235
|
+
|
|
236
|
+
fname : str
|
|
237
|
+
Path/filename
|
|
238
|
+
|
|
239
|
+
Keyword arguments:
|
|
240
|
+
|
|
241
|
+
#flatfile : bool
|
|
242
|
+
asctest : bool
|
|
243
|
+
— Perform character check (assuming it is text)
|
|
244
|
+
'''
|
|
245
|
+
if (kwargs.get('asctest', False)
|
|
246
|
+
or not self.__istext
|
|
247
|
+
or not kwargs.get('flatfile')):
|
|
248
|
+
return []
|
|
249
|
+
outlist = []
|
|
250
|
+
self.__text_obj.seek(0)
|
|
251
|
+
for rown, row in enumerate(self.__text_obj):
|
|
252
|
+
for coln, char in enumerate(row):
|
|
253
|
+
if char not in string.printable and char != '\x00':
|
|
254
|
+
non_asc = {'row':rown+1, 'col': coln+1, 'char':char}
|
|
255
|
+
outlist.append(non_asc)
|
|
256
|
+
self.__text_obj.seek(0)
|
|
257
|
+
return outlist
|
|
258
|
+
|
|
259
|
+
def null_count(self, **kwargs) -> dict: #DONE
|
|
260
|
+
'''
|
|
261
|
+
Returns an integer count of null characters in the file
|
|
262
|
+
('\x00') or None if skipped
|
|
263
|
+
|
|
264
|
+
Keyword arguments:
|
|
265
|
+
|
|
266
|
+
flatfile : bool
|
|
267
|
+
— Test is useless if not a text file. If False, returns 'N/A'
|
|
268
|
+
'''
|
|
269
|
+
if (not kwargs.get('flatfile')
|
|
270
|
+
or not self.__istext
|
|
271
|
+
or not kwargs.get('null_chars')):
|
|
272
|
+
return None
|
|
273
|
+
self.__text_obj.seek(0)
|
|
274
|
+
count = self.__text_obj.read().count('\x00')
|
|
275
|
+
if not count:
|
|
276
|
+
return None
|
|
277
|
+
return count
|
|
278
|
+
|
|
279
|
+
def dos(self, **kwargs) -> bool: #DONE
|
|
280
|
+
'''
|
|
281
|
+
Checks for presence of carriage returns in file
|
|
282
|
+
|
|
283
|
+
Returns True if a carriage return ie, ord(13) is present
|
|
284
|
+
|
|
285
|
+
Keyword arguments:
|
|
286
|
+
|
|
287
|
+
flatfile : bool
|
|
288
|
+
— Perform rectangularity check. If False, returns dictionary
|
|
289
|
+
with all values as 'N/A'
|
|
290
|
+
'''
|
|
291
|
+
if not kwargs.get('flatfile') or not self.__istext:
|
|
292
|
+
return None
|
|
293
|
+
self.__fobj_bin.seek(0)
|
|
294
|
+
for text in self.__fobj_bin:
|
|
295
|
+
if b'\r\n' in text:
|
|
296
|
+
return True
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
def _mime_type(self, fname:pathlib.Path)->tuple:
|
|
300
|
+
'''
|
|
301
|
+
Returns mimetype or 'application/octet-stream'
|
|
302
|
+
'''
|
|
303
|
+
try:
|
|
304
|
+
out = mimetypes.guess_file_type(fname, strict=False)[0]
|
|
305
|
+
except AttributeError:
|
|
306
|
+
#soft deprecation
|
|
307
|
+
out = mimetypes.guess_type(fname)[0]
|
|
308
|
+
if not out:
|
|
309
|
+
out = 'application/octet-stream'
|
|
310
|
+
return out
|
|
311
|
+
|
|
312
|
+
def _report(self, **kwargs) -> dict: #DONE
|
|
313
|
+
'''
|
|
314
|
+
Returns a dictionary of outputs based on keywords below.
|
|
315
|
+
Performs each test and returns the appropriate values. A convenience
|
|
316
|
+
function so that you don't have to run the tests individually.
|
|
317
|
+
|
|
318
|
+
Sample output:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
{'filename':'/tmp/test.csv',
|
|
322
|
+
'flat': True,
|
|
323
|
+
'min_cols': 100, 'max_cols': 100, 'numrec' : 101, 'constant': True,
|
|
324
|
+
'nonascii':False,
|
|
325
|
+
'dos':False}
|
|
326
|
+
```
|
|
327
|
+
Accepted keywords and defaults:
|
|
328
|
+
digest : str
|
|
329
|
+
— Hash algorithm. Default 'md5'
|
|
330
|
+
|
|
331
|
+
flat : bool
|
|
332
|
+
— Flat file checking.
|
|
333
|
+
|
|
334
|
+
nonascii : bool
|
|
335
|
+
— Check for non-ASCII characters.
|
|
336
|
+
|
|
337
|
+
flatfile : bool
|
|
338
|
+
— Perform rectangularity check. If False, returns dictionary
|
|
339
|
+
with all values as 'N/A'
|
|
340
|
+
|
|
341
|
+
null_chars : bool
|
|
342
|
+
- check for null characters
|
|
343
|
+
'''
|
|
344
|
+
out = {'filename': self.fname}
|
|
345
|
+
digest = kwargs.get('digest', 'md5')
|
|
346
|
+
#dos = kwargs.get('dos')
|
|
347
|
+
|
|
348
|
+
out.update({'digestType' : digest})
|
|
349
|
+
out.update({'digest' : self.produce_digest(digest)})
|
|
350
|
+
#out.update({'flat': self.flat_tester(**kwargs)})
|
|
351
|
+
out.update(self.flat_tester(**kwargs))
|
|
352
|
+
#out.update({'flat':'FFFFFFFFFFFF'})
|
|
353
|
+
out.update({'nonascii': self.non_ascii_tester(**kwargs)})
|
|
354
|
+
out.update({'encoding': self.encoding['encoding']})
|
|
355
|
+
out.update({'null_chars': self.null_count(**kwargs)})
|
|
356
|
+
out.update({'mimetype': self._mime_type(self.fname)})
|
|
357
|
+
#if dos:
|
|
358
|
+
# out.update({'dos' : self.dos(**kwargs)})
|
|
359
|
+
#else:
|
|
360
|
+
# out.update({'dos': None})
|
|
361
|
+
out.update({'dos': self.dos(**kwargs)})
|
|
362
|
+
return out
|
|
363
|
+
|
|
364
|
+
def _manifest_txt(self, **kwargs)->str:
|
|
365
|
+
'''
|
|
366
|
+
Returns manifest as plain text
|
|
367
|
+
'''
|
|
368
|
+
return '\n'.join([f'{k}: {v}' for k,v in kwargs['report'].items()
|
|
369
|
+
if v not in ['', None]])
|
|
370
|
+
|
|
371
|
+
def _manifest_json(self, **kwargs)->str:
|
|
372
|
+
'''
|
|
373
|
+
Returns manifest as JSON
|
|
374
|
+
'''
|
|
375
|
+
out = kwargs['report'].copy()
|
|
376
|
+
out['filename'] = str(kwargs['report']['filename'])
|
|
377
|
+
return json.dumps(out)
|
|
378
|
+
|
|
379
|
+
def _manifest_csv(self, **kwargs)->str:
|
|
380
|
+
'''
|
|
381
|
+
Returns manifest as [whatever]-separated value
|
|
382
|
+
'''
|
|
383
|
+
outstr = io.StringIO(newline='')
|
|
384
|
+
writer = csv.DictWriter(outstr, fieldnames=kwargs['report'].keys(),
|
|
385
|
+
delimiter=kwargs.get('sep', ','),
|
|
386
|
+
quoting=csv.QUOTE_MINIMAL)
|
|
387
|
+
if kwargs.get('headers'):
|
|
388
|
+
writer.writeheader()
|
|
389
|
+
writer.writerow(kwargs['report'])
|
|
390
|
+
outstr.seek(0)
|
|
391
|
+
return outstr.read()
|
|
392
|
+
|
|
393
|
+
def manifest(self, **kwargs) -> str: #really as str #DONE
|
|
394
|
+
'''
|
|
395
|
+
Returns desired output type as string
|
|
396
|
+
|
|
397
|
+
out : str
|
|
398
|
+
— Acceptable values are 'txt', 'json', 'csv'
|
|
399
|
+
'txt' Plain text
|
|
400
|
+
'json' JSON
|
|
401
|
+
'csv' Comma-separated value
|
|
402
|
+
|
|
403
|
+
Accepted keywords and defaults:
|
|
404
|
+
|
|
405
|
+
digest : str
|
|
406
|
+
— Hash algorithm. Default 'md5'
|
|
407
|
+
|
|
408
|
+
flat : bool
|
|
409
|
+
— Flat file checking. Default True
|
|
410
|
+
|
|
411
|
+
nonascii : bool
|
|
412
|
+
— Check for non-ASCII characters. Default True
|
|
413
|
+
|
|
414
|
+
dos : bool
|
|
415
|
+
— check for Windows CR/LF combo. Default True
|
|
416
|
+
|
|
417
|
+
flatfile : bool
|
|
418
|
+
— Perform rectangularity check. If False, returns dictionary
|
|
419
|
+
with all values as 'N/A'
|
|
420
|
+
|
|
421
|
+
headers : bool
|
|
422
|
+
— Include csv header (only has any effect with out='csv')
|
|
423
|
+
Default is False
|
|
424
|
+
|
|
425
|
+
sep: str
|
|
426
|
+
— Separator if you want a different plain text separator like a
|
|
427
|
+
tab (\t) or pipe (|). Only functional with csv output, obviously.
|
|
428
|
+
|
|
429
|
+
'''
|
|
430
|
+
report = self._report(**kwargs)
|
|
431
|
+
report_type={'txt': self._manifest_txt,
|
|
432
|
+
'json': self._manifest_json,
|
|
433
|
+
'csv': self._manifest_csv,
|
|
434
|
+
'tsv': self._manifest_csv,
|
|
435
|
+
'psv': self._manifest_csv}
|
|
436
|
+
|
|
437
|
+
try:
|
|
438
|
+
return report_type[kwargs['out']](report=report, **kwargs)
|
|
439
|
+
except KeyError:
|
|
440
|
+
LOGGER.error('Unsupported manifest type %s; defaulting to text', kwargs['out'])
|
|
441
|
+
return report_type[kwargs['out']](report=report, out='txt', **kwargs)
|
|
442
|
+
|
|
443
|
+
if __name__ == '__main__':
|
|
444
|
+
pass
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Manifest generator for data files.
|
|
3
|
+
|
|
4
|
+
Produces a text file with user specified checksums for all files
|
|
5
|
+
from the top of a specified tree and checks line length
|
|
6
|
+
and ASCII character status for text files.
|
|
7
|
+
|
|
8
|
+
For statistics program files:
|
|
9
|
+
SAS .sas7bdat
|
|
10
|
+
SPSS .sav
|
|
11
|
+
Stata .dta
|
|
12
|
+
|
|
13
|
+
Checker() will report number of cases and variables as
|
|
14
|
+
rows and columns respectively.
|
|
15
|
+
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import glob #God I hate Windows
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import pathlib
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
import damage
|
|
26
|
+
|
|
27
|
+
def parse() -> argparse.ArgumentParser(): #DONE
|
|
28
|
+
'''
|
|
29
|
+
Separates argparser into function. Returns arparse.ArgumentParser()
|
|
30
|
+
'''
|
|
31
|
+
desc = ('Produces a text, csv or JSON output with checksums for files, '
|
|
32
|
+
'testing for Windows CRLF combinations, '
|
|
33
|
+
'as well as checking text files for regularity and non/ASCII characters')
|
|
34
|
+
parser = argparse.ArgumentParser(description=desc)
|
|
35
|
+
parser.add_argument('files', help='Files to check. Wildcards acceptable (eg, *)',
|
|
36
|
+
nargs='+', default=' ')
|
|
37
|
+
#note 'prog' is built into argparse
|
|
38
|
+
parser.add_argument('-v', '--version', action='version', version='%(prog)s '+damage.__version__,
|
|
39
|
+
help='Show version number and exit')
|
|
40
|
+
parser.add_argument('-o', '--output', dest='out',
|
|
41
|
+
help='Output format. One of txt, csv, json, tsv',
|
|
42
|
+
default='txt',
|
|
43
|
+
choices = ['txt', 'csv', 'tsv', 'json'],
|
|
44
|
+
type=str.lower)
|
|
45
|
+
parser.add_argument('-n', '--no-flat', action='store_false', dest='flatfile',
|
|
46
|
+
help="Don't check text files for rectangularity")
|
|
47
|
+
parser.add_argument('-r', '--recursive', action='store_true', dest='recur',
|
|
48
|
+
help='Recursive *directory* processing of file tree. Assumes that the '
|
|
49
|
+
'arguments point to a directory (eg, tmp/), and a slash will '
|
|
50
|
+
'be appended if one does not exist')
|
|
51
|
+
parser.add_argument('-t', '--hash-type', dest='digest', default='md5',
|
|
52
|
+
help="Checksum hash type. Supported hashes: 'sha1', "
|
|
53
|
+
"'sha224', 'sha256', 'sha384', 'sha512', 'blake2b', "
|
|
54
|
+
"'blake2s', 'md5'. Default: 'md5'",
|
|
55
|
+
choices = ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
|
|
56
|
+
'blake2b', 'blake2s'],
|
|
57
|
+
type=str.lower)
|
|
58
|
+
parser.add_argument('-a', '--no-ascii', action='store_true', dest='asctest',
|
|
59
|
+
help="Don't check text files for non-ASCII characters")
|
|
60
|
+
parser.add_argument('-f', '--to-file',
|
|
61
|
+
help='Output to -f [file] instead of stdout')
|
|
62
|
+
return parser
|
|
63
|
+
|
|
64
|
+
def recurse_files(inlist) -> map:
|
|
65
|
+
'''
|
|
66
|
+
Returns a map object with pathlib.Paths of files
|
|
67
|
+
'''
|
|
68
|
+
outlist = []
|
|
69
|
+
for flist in inlist:
|
|
70
|
+
rec = os.walk(flist)
|
|
71
|
+
outlist += [pathlib.Path(x[0], y) for x in rec for y in x[2]]
|
|
72
|
+
return outlist #includes hidden files
|
|
73
|
+
|
|
74
|
+
def main(): #pylint: disable=too-many-branches
|
|
75
|
+
'''
|
|
76
|
+
Main function to output manifests to stdout.
|
|
77
|
+
'''
|
|
78
|
+
separator_types = {'csv': ',', 'tsv': '\t'}
|
|
79
|
+
#Purely for formatting output
|
|
80
|
+
line_spacer = {'txt':'\n\n', 'csv':'', 'tsv': ''}
|
|
81
|
+
parser = parse()
|
|
82
|
+
args = parser.parse_args()
|
|
83
|
+
if not args.recur:
|
|
84
|
+
#Windows does not do wildcard expansion at the shell level
|
|
85
|
+
if sys.platform.startswith('win'): #Maybe they will have win64 sometime:
|
|
86
|
+
files = map(pathlib.Path, [y for x in args.files for y in glob.glob(x)])
|
|
87
|
+
else:
|
|
88
|
+
files = map(pathlib.Path, list(args.files))
|
|
89
|
+
else:
|
|
90
|
+
files = recurse_files(args.files)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
output = []
|
|
94
|
+
try: ###
|
|
95
|
+
for num, fil in enumerate(files):
|
|
96
|
+
if not fil.is_file() or not fil.exists():
|
|
97
|
+
continue
|
|
98
|
+
testme = damage.Checker(fil)
|
|
99
|
+
if args.out in separator_types and num == 0:
|
|
100
|
+
output.append(testme.manifest(headers=True,
|
|
101
|
+
sep=separator_types.get(args.out),
|
|
102
|
+
**vars(args)))
|
|
103
|
+
else:
|
|
104
|
+
output.append(testme.manifest(sep=separator_types.get(args.out),
|
|
105
|
+
**vars(args)))
|
|
106
|
+
if not args.out == 'json':
|
|
107
|
+
#print(line_spacer[args.out].join(output).strip())
|
|
108
|
+
out_info =line_spacer[args.out].join(output).strip()
|
|
109
|
+
else:
|
|
110
|
+
outjson = ('{"files" :' +
|
|
111
|
+
'[' + ','.join(output) + ']'
|
|
112
|
+
+ '}')
|
|
113
|
+
out_info = json.dumps(json.loads(outjson)) #validate
|
|
114
|
+
except Exception as err: #pylint: disable=broad-exception-caught
|
|
115
|
+
print(f'Abnormal program termination {err}')
|
|
116
|
+
sys.exit()
|
|
117
|
+
|
|
118
|
+
if args.to_file:
|
|
119
|
+
with open(pathlib.Path(args.to_file), mode='w',
|
|
120
|
+
encoding='utf-8') as outf:
|
|
121
|
+
outf.write(out_info)
|
|
122
|
+
else:
|
|
123
|
+
print(out_info)
|
|
124
|
+
|
|
125
|
+
if __name__ == '__main__':
|
|
126
|
+
main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2025 University of British Columbia Library
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
damage/gui/damage_gui.py
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Damage GUI application
|
|
3
|
+
'''
|
|
4
|
+
#import base64
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import pathlib
|
|
9
|
+
import shlex
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
import textwrap
|
|
15
|
+
import webbrowser
|
|
16
|
+
import FreeSimpleGUI as sg
|
|
17
|
+
|
|
18
|
+
import damage
|
|
19
|
+
#pylint: disable = possibly-used-before-assignment
|
|
20
|
+
|
|
21
|
+
#TODO put the application into a class or function
|
|
22
|
+
#Prettify the name
|
|
23
|
+
PROGNAME = pathlib.Path(__file__).stem.capitalize().replace('_gui','')
|
|
24
|
+
__version__ = '.'.join([str(x) for x in damage.VERSION])
|
|
25
|
+
|
|
26
|
+
BASEDIR = pathlib.Path(__file__).parent
|
|
27
|
+
|
|
28
|
+
with open(pathlib.Path(BASEDIR, 'assets', 'DamageAppIcon.png'),
|
|
29
|
+
'rb') as f:
|
|
30
|
+
ICON = base64.b64encode(f.read())
|
|
31
|
+
|
|
32
|
+
with open(pathlib.Path(BASEDIR, 'assets', 'LICENCE.txt'),
|
|
33
|
+
mode='r', encoding='utf-8') as lic:
|
|
34
|
+
LICENCE = textwrap.fill(lic.read(), width=70,
|
|
35
|
+
replace_whitespace=False)
|
|
36
|
+
|
|
37
|
+
sg.DEFAULT_WINDOW_ICON = ICON
|
|
38
|
+
|
|
39
|
+
if sg.running_mac():
|
|
40
|
+
import plistlib
|
|
41
|
+
TTK_THEME = 'aqua'
|
|
42
|
+
# make FONTSIZE dependent on homebrew in path because only brew python
|
|
43
|
+
# compiles successfully on MacOS on a case-insensitive file system
|
|
44
|
+
# and for some inconceivable reason the font size changes
|
|
45
|
+
FONTSIZE = 14
|
|
46
|
+
for hack in ['homebrew', 'MacOS']:
|
|
47
|
+
if hack in {y for x in sys.path for y in pathlib.Path(x).parts}:
|
|
48
|
+
FONTSIZE = 10
|
|
49
|
+
BASEFONT = 'System'
|
|
50
|
+
MOD = '\u2318' #CMD key unicode 2318 Place of Interest
|
|
51
|
+
CMDCTRL = 'Command' #tkinter bind string sans <>
|
|
52
|
+
|
|
53
|
+
if sg.running_windows():
|
|
54
|
+
TTK_THEME = 'vista'
|
|
55
|
+
FONTSIZE = 9
|
|
56
|
+
BASEFONT = 'Arial' #GRR #TODO no longer basefont on Windows
|
|
57
|
+
MOD = 'Ctrl-'
|
|
58
|
+
CMDCTRL = 'Control'
|
|
59
|
+
|
|
60
|
+
if sg.running_linux():
|
|
61
|
+
TTK_THEME = 'alt'
|
|
62
|
+
FONTSIZE = 9
|
|
63
|
+
BASEFONT = 'TkDefaultFont' #GRR
|
|
64
|
+
MOD = 'Ctrl-'
|
|
65
|
+
CMDCTRL = 'Control'
|
|
66
|
+
sg.set_options(font=f'{BASEFONT} {FONTSIZE}')
|
|
67
|
+
|
|
68
|
+
sg.theme('systemDefaultForReal')
|
|
69
|
+
|
|
70
|
+
# I dislike having this as a global variable
|
|
71
|
+
# TODO find a better way to store PREFDICT
|
|
72
|
+
PREFDICT = None
|
|
73
|
+
|
|
74
|
+
def debug_mode():
|
|
75
|
+
'''
|
|
76
|
+
Turn on debugger from the command line
|
|
77
|
+
'''
|
|
78
|
+
yes_to_debug = ['debug', 'd', '1']
|
|
79
|
+
if len(sys.argv) > 1:
|
|
80
|
+
debug = sys.argv[1]
|
|
81
|
+
if debug.lower() in yes_to_debug:
|
|
82
|
+
sg.show_debugger_window(location=(10,10))
|
|
83
|
+
|
|
84
|
+
def is_csv(indict:dict)->bool:
|
|
85
|
+
'''
|
|
86
|
+
Convenience function to see if output is csv
|
|
87
|
+
|
|
88
|
+
'''
|
|
89
|
+
#I really need to redo the whole works from scratch
|
|
90
|
+
if indict.get('out') == 'csv':
|
|
91
|
+
return True
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def pref_path()->pathlib.Path:
|
|
95
|
+
'''
|
|
96
|
+
Returns path to preferences directory and preferences
|
|
97
|
+
file name as a tuple
|
|
98
|
+
'''
|
|
99
|
+
if sg.running_mac():
|
|
100
|
+
return pathlib.Path(pathlib.Path('~/Library/Preferences').expanduser(),
|
|
101
|
+
'ca.ubc.library.damage.prefs.plist')
|
|
102
|
+
if sg.running_windows():
|
|
103
|
+
return pathlib.Path(pathlib.Path('~/AppData/Local/damage').expanduser(), 'damage.json')
|
|
104
|
+
#Linux and everything else
|
|
105
|
+
return pathlib.Path(pathlib.Path('~/.config/damage').expanduser(),
|
|
106
|
+
'damage.json')
|
|
107
|
+
|
|
108
|
+
def get_prefs()->None:
|
|
109
|
+
'''
|
|
110
|
+
Gets preferences from JSON or default dict. If no preferences
|
|
111
|
+
file is found, one is written
|
|
112
|
+
'''
|
|
113
|
+
global PREFDICT
|
|
114
|
+
try:
|
|
115
|
+
if sg.running_mac():
|
|
116
|
+
with open(pref_path(), 'rb') as fn:
|
|
117
|
+
PREFDICT = plistlib.load(fn)
|
|
118
|
+
if sg.running_linux() or sg.running_windows():
|
|
119
|
+
with open(pref_path(), encoding='utf-8') as fn:
|
|
120
|
+
PREFDICT = sg.json.load(fn)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
except FileNotFoundError:
|
|
124
|
+
PREFDICT = { 'flatfile' :False,
|
|
125
|
+
'recurse' :False,
|
|
126
|
+
'digest' :'md5',
|
|
127
|
+
'out' :'txt',
|
|
128
|
+
'short' :True,
|
|
129
|
+
'headers' :True,
|
|
130
|
+
'nonascii' :True,
|
|
131
|
+
'hidden' : False}
|
|
132
|
+
|
|
133
|
+
fixflat = PREFDICT.get('flat')
|
|
134
|
+
if fixflat:
|
|
135
|
+
PREFDICT['flatfile'] = fixflat
|
|
136
|
+
del PREFDICT['flat']
|
|
137
|
+
# Add new keys here when expanding prefs
|
|
138
|
+
PREFDICT['hidden'] = PREFDICT.get('hidden', False)
|
|
139
|
+
|
|
140
|
+
def set_prefs()->None:
|
|
141
|
+
'''
|
|
142
|
+
Sets preferences
|
|
143
|
+
'''
|
|
144
|
+
if not pref_path().parent.exists():
|
|
145
|
+
os.makedirs(pref_path().parent)
|
|
146
|
+
if sg.running_mac():
|
|
147
|
+
with open(pref_path(), 'wb') as fn:
|
|
148
|
+
plistlib.dump(PREFDICT, fn)
|
|
149
|
+
if sg.running_linux() or sg.running_windows():
|
|
150
|
+
with open(pref_path(), 'w', encoding='utf-8') as fn:
|
|
151
|
+
sg.json.dump(PREFDICT, fn)
|
|
152
|
+
|
|
153
|
+
def damager(flist, **kwargs)->str:
|
|
154
|
+
'''
|
|
155
|
+
Text output from Damage utility
|
|
156
|
+
'''
|
|
157
|
+
output = []
|
|
158
|
+
for num, fil in enumerate(flist):
|
|
159
|
+
if not pathlib.Path(fil).is_file() or not pathlib.Path(fil).exists():
|
|
160
|
+
continue
|
|
161
|
+
testme = damage.Checker(fil)
|
|
162
|
+
if kwargs['out'] == 'csv' and num == 0:
|
|
163
|
+
kwargs['headers'] = True
|
|
164
|
+
output.append(testme.manifest(**kwargs))
|
|
165
|
+
else:
|
|
166
|
+
kwargs['headers'] = False
|
|
167
|
+
output.append(testme.manifest(**kwargs))
|
|
168
|
+
if kwargs['out'] =='txt':
|
|
169
|
+
return '\n\n'.join(output)
|
|
170
|
+
if kwargs['out'] == 'csv':
|
|
171
|
+
return ''.join(output)#Horrible hack; excel dialiect automatically adds \r\n
|
|
172
|
+
outjson = ('{"files" :' +
|
|
173
|
+
'[' + ','.join(output) + ']'
|
|
174
|
+
+ '}')
|
|
175
|
+
outjson = json.dumps(json.loads(outjson)) #validate
|
|
176
|
+
return outjson
|
|
177
|
+
|
|
178
|
+
def get_folder_files(direc:str, recursive:bool=False, hidden:bool=False)->list:
|
|
179
|
+
'''
|
|
180
|
+
Returns files in a folder, recursive or no
|
|
181
|
+
direc : str
|
|
182
|
+
path to directory
|
|
183
|
+
recursive : bool
|
|
184
|
+
Return a recursive result. Default False
|
|
185
|
+
hidden : bool
|
|
186
|
+
Show hidden files. Default False
|
|
187
|
+
'''
|
|
188
|
+
#HACKME
|
|
189
|
+
if not hidden:
|
|
190
|
+
hidden = PREFDICT.get('hidden', False)
|
|
191
|
+
if not direc:#Possible if window call cancelled, as I have discovered
|
|
192
|
+
#return None
|
|
193
|
+
return []
|
|
194
|
+
walker = pathlib.Path(direc).walk()
|
|
195
|
+
if not recursive:
|
|
196
|
+
walker = [next(walker)]
|
|
197
|
+
else:
|
|
198
|
+
walker = list(walker)
|
|
199
|
+
flist=[]
|
|
200
|
+
#Hidden is a property in the Checker object, but this comes before instantiation
|
|
201
|
+
if hidden:
|
|
202
|
+
#flist = [[x[0], pathlib.Path(x[0], y).name] for x in walker for y in x[2]
|
|
203
|
+
# if pathlib.Path(x[0],y).is_file()]
|
|
204
|
+
flist = [pathlib.Path(x[0], y) for x in walker for y in x[2]
|
|
205
|
+
if pathlib.Path(x[0],y).is_file()]
|
|
206
|
+
|
|
207
|
+
else:
|
|
208
|
+
#hidden defined as any part of the path starts with '.'
|
|
209
|
+
flist = [pathlib.Path(x[0],y) for x in walker for y in x[2]
|
|
210
|
+
if not any(z.startswith('.') for z in
|
|
211
|
+
pathlib.Path(x[0], y).parts)]
|
|
212
|
+
return flist
|
|
213
|
+
|
|
214
|
+
def send_to_file(outstring)->None:
|
|
215
|
+
'''
|
|
216
|
+
Sends string output to file
|
|
217
|
+
|
|
218
|
+
Creates a tk.asksaveasfile dialogue and saves
|
|
219
|
+
'''
|
|
220
|
+
#Because TK is just easier
|
|
221
|
+
outfile = sg.tk.filedialog.asksaveasfile(title='Save Output',
|
|
222
|
+
initialfile=f'output.{PREFDICT["out"]}',
|
|
223
|
+
confirmoverwrite=True)
|
|
224
|
+
if outfile:
|
|
225
|
+
outfile.write(outstring)
|
|
226
|
+
outfile.close()
|
|
227
|
+
|
|
228
|
+
def send_to_printer(outstring:str)->None:
|
|
229
|
+
'''
|
|
230
|
+
Sends output to lpr on Mac/linux and to default printer in Windows
|
|
231
|
+
Data is unformatted. If you want formatting save to a file and use
|
|
232
|
+
a text editor. Assumes UTF-8 for Mac/linux.
|
|
233
|
+
'''
|
|
234
|
+
#https://stackoverflow.com/questions/12723818/print-to-standard-printer-from-python
|
|
235
|
+
#outfile = tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
|
236
|
+
# suffix='.txt', delete=False)
|
|
237
|
+
#outfile.write(outstring)
|
|
238
|
+
#outfile.close()
|
|
239
|
+
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
|
|
240
|
+
suffix='.txt', delete=False) as outfile:
|
|
241
|
+
outfile.write(outstring)
|
|
242
|
+
|
|
243
|
+
if sg.running_mac() or sg.running_linux():
|
|
244
|
+
#lpr = subprocess.Popen(shutil.which('lpr'), stdin=subprocess.PIPE)
|
|
245
|
+
#lpr.stdin.write(bytes(outstring, 'utf-8'))
|
|
246
|
+
subprocess.run([shutil.which('lpr'), outfile.name], check=False)
|
|
247
|
+
|
|
248
|
+
if sg.running_windows():
|
|
249
|
+
|
|
250
|
+
#List of all printers names and shows default one
|
|
251
|
+
#wmic printer get name,default
|
|
252
|
+
#https://stackoverflow.com/questions/13311201/get-default-printer-name-from-command-line
|
|
253
|
+
subout = subprocess.run(shlex.split('wmic printer get name,default'),
|
|
254
|
+
capture_output=True,
|
|
255
|
+
check=False)
|
|
256
|
+
#the following only makes sense of you look at the output of the
|
|
257
|
+
#windows shell command above. stout is binary, hence decode.
|
|
258
|
+
printerinfo = [[x[:6].strip(), x[6:].strip()] for x in
|
|
259
|
+
subout.stdout.decode().split('\n')[1:]]
|
|
260
|
+
default_printer = [x for x in printerinfo if x[0] == 'TRUE'][0][1]
|
|
261
|
+
subprocess.run(['print', f'/D:{default_printer}', outfile.name], check=False)
|
|
262
|
+
#tempfile must be removed manually because of delete=False above
|
|
263
|
+
os.remove(outfile.name)
|
|
264
|
+
sg.popup('Output sent to default printer', title='Job Completed', any_key_closes=True)
|
|
265
|
+
|
|
266
|
+
def about_window()->sg.Window:
|
|
267
|
+
'''
|
|
268
|
+
Creates the "About" window
|
|
269
|
+
'''
|
|
270
|
+
about = { 'developers' : ['Paul Lesack'],
|
|
271
|
+
'user_testers' : ['Alex Alisauskas','Jeremy Buhler', 'Cheryl Niamath'],
|
|
272
|
+
'source_url' : 'https://github.com/ubc-library-rc/damage',
|
|
273
|
+
'documentation' : 'https://ubc-library-rc.github.io/damage'
|
|
274
|
+
}
|
|
275
|
+
#displayname = f'{PROGNAME[:PROGNAME.find("_")].capitalize()} v{__version__}'
|
|
276
|
+
displayname = f'{PROGNAME} v{__version__}'
|
|
277
|
+
name = [[sg.Text(displayname, font=f'{BASEFONT} {FONTSIZE+4} bold')]]
|
|
278
|
+
source =[[sg.Text('Source code', font=f'{BASEFONT} {FONTSIZE+2} bold')],
|
|
279
|
+
[sg.Text(about['source_url'], enable_events=True, text_color='blue', k='-SC-')]]
|
|
280
|
+
documentation =[[sg.Text('Documentation', font=f'{BASEFONT} {FONTSIZE+2} bold')],
|
|
281
|
+
[sg.Text(about['source_url'], enable_events=True,
|
|
282
|
+
text_color='blue', k='-DOC-')]]
|
|
283
|
+
devs =[[sg.Text('Developers', font=f'{BASEFONT} {FONTSIZE+2} bold',)],
|
|
284
|
+
[sg.Text(x) for x in about['developers']]]
|
|
285
|
+
testers = [[sg.Text('Testers', font=f'{BASEFONT} {FONTSIZE+2} bold')],
|
|
286
|
+
[sg.Text(x) for x in about['user_testers']]]
|
|
287
|
+
licence =[[sg.Text('Licence information', font=f'{BASEFONT} {FONTSIZE+2} bold')],
|
|
288
|
+
[sg.Text(LICENCE, font=f'{BASEFONT} {FONTSIZE-2}')]]
|
|
289
|
+
layout = name + documentation + devs + testers + source + licence
|
|
290
|
+
window = sg.Window('Credits & Details', modal=True,
|
|
291
|
+
icon=ICON,
|
|
292
|
+
keep_on_top=True,
|
|
293
|
+
layout=layout,
|
|
294
|
+
finalize=True)
|
|
295
|
+
while True:
|
|
296
|
+
#event, values = window.read()
|
|
297
|
+
# don't need values
|
|
298
|
+
event = window.read()[0]
|
|
299
|
+
if event == '-SC-':
|
|
300
|
+
webbrowser.open(about['source_url'])
|
|
301
|
+
if event == '-DOC-':
|
|
302
|
+
webbrowser.open(about['documentation'])
|
|
303
|
+
if event == sg.WIN_CLOSED:
|
|
304
|
+
break
|
|
305
|
+
window.close()
|
|
306
|
+
|
|
307
|
+
def prefs_window()->sg.Window:
|
|
308
|
+
'''
|
|
309
|
+
Creates a preferences popup window.
|
|
310
|
+
Values from window saved to the preferences dictionary PREFDICT
|
|
311
|
+
'''
|
|
312
|
+
#All the options
|
|
313
|
+
hashes =['md5','sha1', 'sha224', 'sha256', 'sha384', 'sha512',
|
|
314
|
+
'blake2b', 'blake2s']
|
|
315
|
+
outputs = ['txt','csv', 'json']
|
|
316
|
+
rectang = ('Text file rectangularity '
|
|
317
|
+
'& statistics file column check' )
|
|
318
|
+
layout = [[sg.Text('Damage Preferences', font=f'{BASEFONT} {FONTSIZE+4} bold')],
|
|
319
|
+
[sg.Checkbox('Shorten file paths in output',
|
|
320
|
+
key= '-SHORT-',
|
|
321
|
+
default=PREFDICT['short'], )],
|
|
322
|
+
[sg.Checkbox(text=rectang,
|
|
323
|
+
key= '-FLATFILE-',
|
|
324
|
+
default=PREFDICT['flatfile'], )],
|
|
325
|
+
[sg.Checkbox('Recursively add files from directories',
|
|
326
|
+
key='-RECURSE-', default=PREFDICT['recurse'])],
|
|
327
|
+
[sg.Checkbox('Include hidden files',
|
|
328
|
+
key='-HIDDEN-', default=PREFDICT['hidden'])],
|
|
329
|
+
[sg.Text('Hash type'),
|
|
330
|
+
sg.Combo(values=hashes, default_value=PREFDICT['digest'],
|
|
331
|
+
key='-DIGEST-', readonly=True)],
|
|
332
|
+
[sg.Text('Output format'),
|
|
333
|
+
sg.Combo(values=outputs, default_value=PREFDICT['out'],
|
|
334
|
+
key='-OUT-', readonly=True)],
|
|
335
|
+
[sg.Ok(bind_return_key=True, button_text='OK')]]
|
|
336
|
+
pwindow = sg.Window(title='Preferences',
|
|
337
|
+
icon=ICON,
|
|
338
|
+
resizable=True,
|
|
339
|
+
layout=layout,
|
|
340
|
+
ttk_theme=TTK_THEME,
|
|
341
|
+
use_ttk_buttons=True,
|
|
342
|
+
keep_on_top=True,
|
|
343
|
+
modal=True, finalize=True)
|
|
344
|
+
pwindow.bind('<Escape>', 'Exit')
|
|
345
|
+
pevent, pvalues = pwindow.read()
|
|
346
|
+
if pevent:
|
|
347
|
+
for key in ['short', 'flatfile', 'recurse', 'digest', 'out', 'hidden']:
|
|
348
|
+
PREFDICT[key] = pvalues[f'-{key.upper()}-']
|
|
349
|
+
set_prefs()
|
|
350
|
+
if pevent == 'Exit':
|
|
351
|
+
pwindow.close()
|
|
352
|
+
pwindow.close()
|
|
353
|
+
|
|
354
|
+
def window_binds(window:sg.Window)->None:
|
|
355
|
+
'''
|
|
356
|
+
Bind keys to main window
|
|
357
|
+
'''
|
|
358
|
+
window.bind(f'<{CMDCTRL}-v>', '-PASTE-')
|
|
359
|
+
window.bind(f'<{CMDCTRL}-s>', '-SAVE-')
|
|
360
|
+
window.bind(f'<{CMDCTRL}-p>', '-PRINT-')
|
|
361
|
+
window.bind(f'<{CMDCTRL}-m>', '-MANIFEST-')
|
|
362
|
+
|
|
363
|
+
def main_window()->sg.Window:
|
|
364
|
+
'''
|
|
365
|
+
The main damage window
|
|
366
|
+
'''
|
|
367
|
+
#def get_folder_items()->list:
|
|
368
|
+
# '''
|
|
369
|
+
# Get all items from a folder listing
|
|
370
|
+
# '''
|
|
371
|
+
#I gave up attaching preferences to the mac menu
|
|
372
|
+
#because there's too much hidden using sg.
|
|
373
|
+
#You may as well use straight tk if you want that.
|
|
374
|
+
|
|
375
|
+
#Menu section
|
|
376
|
+
debug_mode()
|
|
377
|
+
menu = sg.Menu([['File',['Add &Files',
|
|
378
|
+
'Add Fol&der',
|
|
379
|
+
'Remove Files::-DELETE-']],
|
|
380
|
+
['Edit', [f'&Copy {MOD}C::-COPY-',
|
|
381
|
+
f'&Paste {MOD}V::-PASTE-',
|
|
382
|
+
'Preferences']],
|
|
383
|
+
['Actions', [f'Create &Manifest {MOD}M::-MANIFEST-',
|
|
384
|
+
'---',
|
|
385
|
+
f'!&Save Output to File {MOD}S::-SAVE-',
|
|
386
|
+
f'!&Print Output {MOD}P::-PRINT-']],
|
|
387
|
+
['Help', ['Damage Help', 'Credits and Details']]],
|
|
388
|
+
key='-MENUBAR-')
|
|
389
|
+
#Chosen file (left) section
|
|
390
|
+
lbox = sg.Listbox(values=[], key='-SELECT-',
|
|
391
|
+
enable_events=True,
|
|
392
|
+
size=20,
|
|
393
|
+
select_mode=sg.LISTBOX_SELECT_MODE_EXTENDED,
|
|
394
|
+
horizontal_scroll=True,
|
|
395
|
+
expand_y=True,
|
|
396
|
+
expand_x=True)
|
|
397
|
+
left = [[lbox],
|
|
398
|
+
[sg.Input(visible=False, enable_events=True, key='-IN-'),
|
|
399
|
+
sg.FilesBrowse(button_text='Add Files', target='-IN-'),
|
|
400
|
+
sg.Input(visible=False, enable_events=True, key='-IFOLD-'),
|
|
401
|
+
sg.FolderBrowse(button_text='Add Folder', key='-FOLDER-',
|
|
402
|
+
target='-IFOLD-',
|
|
403
|
+
initial_folder=pathlib.Path('~').expanduser()),
|
|
404
|
+
sg.Button(button_text='Remove Files',
|
|
405
|
+
enable_events=True, key='-DELETE-')]]
|
|
406
|
+
|
|
407
|
+
#Right side (output) section
|
|
408
|
+
#This is more complicated than it should be because psg doesn't
|
|
409
|
+
#flip correctly between Multiline and Table elements and doesn't hide
|
|
410
|
+
#the Table scrollbars, and resizing is also affected. The (a) solution
|
|
411
|
+
#is to put each of them in a Frame.
|
|
412
|
+
#But the best solution would be to use straight tkinter but it's too
|
|
413
|
+
#late for that, isn't it.
|
|
414
|
+
|
|
415
|
+
outbox = sg.Multiline(key='-OUTPUT-',
|
|
416
|
+
size=10000, #Auto-expansion has a bug
|
|
417
|
+
expand_x=True,
|
|
418
|
+
expand_y=True,
|
|
419
|
+
visible=True,
|
|
420
|
+
auto_refresh=True)
|
|
421
|
+
frame_1=sg.Frame(title ='',
|
|
422
|
+
layout=[[outbox]],
|
|
423
|
+
expand_x=True,
|
|
424
|
+
expand_y=True,
|
|
425
|
+
border_width=0,
|
|
426
|
+
key='F1',
|
|
427
|
+
visible=not is_csv(PREFDICT))
|
|
428
|
+
|
|
429
|
+
#CSV/tabular output box
|
|
430
|
+
#hardcoding the headers seems dumb,
|
|
431
|
+
#but the headers have to be read *after* instantiation of a Checker object
|
|
432
|
+
#TODONE used these ones for now, not really happy about it
|
|
433
|
+
headers = ['filename', 'digestType', 'digest',
|
|
434
|
+
'min_cols', 'max_cols', 'numrec',
|
|
435
|
+
'constant', 'encoding', 'nonascii',
|
|
436
|
+
'null_chars', 'mimetype', 'dos']
|
|
437
|
+
|
|
438
|
+
#right side layout
|
|
439
|
+
csvout = sg.Table([['' for x in range(len(headers))]], #Need data to create the table
|
|
440
|
+
headings=headers,
|
|
441
|
+
key='-CSV-',
|
|
442
|
+
#num_rows=40, #not needed with expansion
|
|
443
|
+
alternating_row_color='#FFA805',
|
|
444
|
+
#size=(800,40),
|
|
445
|
+
expand_x=True,
|
|
446
|
+
expand_y=True,
|
|
447
|
+
hide_vertical_scroll=False,
|
|
448
|
+
vertical_scroll_only=False,
|
|
449
|
+
auto_size_columns=True,
|
|
450
|
+
#def_col_width=200,#If this is at default window expansion doesn't work
|
|
451
|
+
#max_col_width=2000,#This ought to be enough
|
|
452
|
+
#visible=is_csv(PREFDICT),
|
|
453
|
+
visible=True,
|
|
454
|
+
enable_events=True
|
|
455
|
+
)
|
|
456
|
+
frame_2=sg.Frame(title ='',
|
|
457
|
+
layout=[[csvout]],
|
|
458
|
+
expand_x=True,
|
|
459
|
+
expand_y=True,
|
|
460
|
+
border_width=0,
|
|
461
|
+
key='F2',
|
|
462
|
+
visible=True)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
right = [[frame_1, frame_2],
|
|
466
|
+
[sg.Text(expand_x=True),
|
|
467
|
+
sg.Button('Generate Manifest',
|
|
468
|
+
key='-MANIFEST-') ]]
|
|
469
|
+
|
|
470
|
+
#Main window layout using all the elements above
|
|
471
|
+
layout = [[menu],
|
|
472
|
+
[sg.Frame(title='File Section',
|
|
473
|
+
layout=left,
|
|
474
|
+
size=(400,800),
|
|
475
|
+
expand_y=True,
|
|
476
|
+
expand_x=True),
|
|
477
|
+
sg.Frame(title='Output Section',
|
|
478
|
+
size=(800,800),
|
|
479
|
+
layout=right,
|
|
480
|
+
expand_y=True,
|
|
481
|
+
expand_x=True)]]
|
|
482
|
+
|
|
483
|
+
window = sg.Window(title='Damage', layout=layout, resizable=True,
|
|
484
|
+
size = PREFDICT.get('main_size', (None, None)),
|
|
485
|
+
icon=ICON,
|
|
486
|
+
ttk_theme=TTK_THEME,
|
|
487
|
+
use_ttk_buttons=True,
|
|
488
|
+
location= PREFDICT.get('main_location', (None, None)),
|
|
489
|
+
finalize=True, enable_close_attempted_event=True)
|
|
490
|
+
lbox.Widget.config(borderwidth=0)
|
|
491
|
+
window.set_min_size((875,400))
|
|
492
|
+
window_binds(window)
|
|
493
|
+
return window
|
|
494
|
+
|
|
495
|
+
def main()->None:
|
|
496
|
+
'''
|
|
497
|
+
Main loop
|
|
498
|
+
'''
|
|
499
|
+
get_prefs()
|
|
500
|
+
window = main_window()
|
|
501
|
+
#FFFFUUUUU
|
|
502
|
+
menulayout = window['-MENUBAR-'].MenuDefinition
|
|
503
|
+
#sg.easy_print(menulayout)
|
|
504
|
+
#Why don't I just do it all in TK? Jesus
|
|
505
|
+
#talk about undocumented.
|
|
506
|
+
#also: https://tkdocs.com/tutorial/menus.html
|
|
507
|
+
#window.TKroot.tk.createcommand('tk::mac::ShowPreferences', prefs_window )
|
|
508
|
+
# How to get root? What is the function where None is?
|
|
509
|
+
#root = window.hidden_master_root #(see PySimpleGUI.py.StartupTK, line 16008)
|
|
510
|
+
#sg.easy_print(type(poot))
|
|
511
|
+
#root.createcommand('tk::mac::ShowPreferences', prefs_window )
|
|
512
|
+
#root.createcommand('tk::mac::ShowPreferences', lambda: None)
|
|
513
|
+
#Also, why does the window stop responding after calling the prefs? But only half? It's
|
|
514
|
+
#a fucking mystery. And it doesn't happen with straight TK so it's something to with psg.
|
|
515
|
+
#root.createcommand('tk::mac::standardAboutPanel', about_window)
|
|
516
|
+
while True:
|
|
517
|
+
event, values = window.read()
|
|
518
|
+
|
|
519
|
+
if event in (sg.WINDOW_CLOSE_ATTEMPTED_EVENT,):
|
|
520
|
+
PREFDICT['main_size'] = window.size
|
|
521
|
+
PREFDICT['main_location'] = window.current_location()
|
|
522
|
+
set_prefs()
|
|
523
|
+
break
|
|
524
|
+
|
|
525
|
+
if event in (sg.WINDOW_CLOSED,):
|
|
526
|
+
break
|
|
527
|
+
|
|
528
|
+
if event == '-IN-':
|
|
529
|
+
#sg.easy_print(f"Values=| {values['-IN-']} |", c='white on red')
|
|
530
|
+
upd_list = (window['-SELECT-'].get_list_values() +
|
|
531
|
+
[x for x in values['-IN-'].split(';') if
|
|
532
|
+
x not in window['-SELECT-'].get_list_values()])
|
|
533
|
+
if PREFDICT.get('hidden', False):
|
|
534
|
+
upd_list = [x for x in upd_list if pathlib.Path(x).is_file()]
|
|
535
|
+
else:
|
|
536
|
+
upd_list = [x for x in upd_list if pathlib.Path(x).is_file()
|
|
537
|
+
and not any(z.startswith('.')
|
|
538
|
+
for z in pathlib.Path(x).parts)]
|
|
539
|
+
window['-SELECT-'].update(upd_list)
|
|
540
|
+
window['-IN-'].update(value='')
|
|
541
|
+
|
|
542
|
+
if event in ('-IFOLD-', 'Add Folder'):
|
|
543
|
+
if event in ('Add Folder',):
|
|
544
|
+
#Not implementing same behaviour in button
|
|
545
|
+
#requires rewriting menu
|
|
546
|
+
#TODO find a way to have the menu item
|
|
547
|
+
#and button have the behaviour below
|
|
548
|
+
#probably by having the button replaced by a function call.
|
|
549
|
+
initiald = pathlib.Path('~').expanduser()
|
|
550
|
+
newfiles = get_folder_files(sg.popup_get_folder('',
|
|
551
|
+
no_window=True,
|
|
552
|
+
initial_folder=initiald),
|
|
553
|
+
PREFDICT['recurse'])
|
|
554
|
+
else:
|
|
555
|
+
newfiles = get_folder_files(values['-IFOLD-'], PREFDICT['recurse'])
|
|
556
|
+
upd_list = window['-SELECT-'].get_list_values()
|
|
557
|
+
#sg.easy_print('Used Add Folder Button')
|
|
558
|
+
upd_list = upd_list+ [x for x in newfiles if x not in upd_list]
|
|
559
|
+
window['-SELECT-'].update(upd_list)
|
|
560
|
+
|
|
561
|
+
if event.endswith('-DELETE-'):
|
|
562
|
+
nlist = [x for x in window['-SELECT-'].get_list_values() if
|
|
563
|
+
x not in values['-SELECT-']]
|
|
564
|
+
window['-SELECT-'].update(nlist)
|
|
565
|
+
|
|
566
|
+
if event.endswith('-MANIFEST-'):
|
|
567
|
+
#sg.easy_print(event, values)
|
|
568
|
+
delme = ''
|
|
569
|
+
#Only clear manifest
|
|
570
|
+
window['-OUTPUT-'].update(delme)#clear first
|
|
571
|
+
window['-CSV-'].update(delme)#clear first
|
|
572
|
+
try:
|
|
573
|
+
upd_list = window['-SELECT-'].get_list_values()
|
|
574
|
+
if upd_list == ['']: #why
|
|
575
|
+
upd_list = []
|
|
576
|
+
if upd_list:
|
|
577
|
+
txt = damager(upd_list, **PREFDICT)
|
|
578
|
+
if PREFDICT['short'] and len(upd_list) > 1:
|
|
579
|
+
#commonpath does not include last sep, very annoying
|
|
580
|
+
delme = os.path.commonpath(upd_list) + os.sep
|
|
581
|
+
window['-OUTPUT-'].update(txt.replace(delme,''))
|
|
582
|
+
elif PREFDICT['short'] and len(upd_list) == 1:
|
|
583
|
+
#delme = os.path.split(upd_list[0])[0] + os.sep
|
|
584
|
+
delme = pathlib.Path(upd_list[0]).parts[0] + os.sep #consistency
|
|
585
|
+
txt = txt.replace(delme,'')
|
|
586
|
+
window['-OUTPUT-'].update(txt)
|
|
587
|
+
|
|
588
|
+
if PREFDICT.get('out') == 'csv':
|
|
589
|
+
txt = txt.split('\n')
|
|
590
|
+
#reader=csv.reader(txt[1::2], delimiter=',')#Strip out the headers
|
|
591
|
+
#window['-CSV-'].update(values=list(reader))
|
|
592
|
+
#nout = [txt[0]] + txt[1::2]#combine header with data
|
|
593
|
+
##and send to output window for printing/saving
|
|
594
|
+
#window['-OUTPUT-'].update('\n'.join(nout))
|
|
595
|
+
|
|
596
|
+
#new
|
|
597
|
+
#This is like it is because window['-CSV-'] takes lists
|
|
598
|
+
#as values and window['-OUTPUT-] is text output
|
|
599
|
+
#and csv.Reader adds windows \r\n.
|
|
600
|
+
nout = [x.strip().split(',') for x in txt[1:]]
|
|
601
|
+
nout = [x for x in nout if x !=['']]
|
|
602
|
+
window['-CSV-'].update(nout)
|
|
603
|
+
nout = [txt[0].strip()]+[','.join(z) for z in nout]
|
|
604
|
+
nout = '\n'.join(nout)
|
|
605
|
+
window['-OUTPUT-'].update(nout)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
except (ValueError, NameError, AttributeError):
|
|
610
|
+
window['-OUTPUT-'].update(delme)
|
|
611
|
+
|
|
612
|
+
if event == 'Preferences':
|
|
613
|
+
#sg.easy_print(values)
|
|
614
|
+
prefs_window()
|
|
615
|
+
#Show correct window pane for output.
|
|
616
|
+
xpandr = is_csv(PREFDICT)
|
|
617
|
+
window['F1'].update(visible=not xpandr)
|
|
618
|
+
window['F2'].update(visible=xpandr)
|
|
619
|
+
window['F1'].expand(expand_x=not xpandr, expand_y=not xpandr)
|
|
620
|
+
window['F2'].expand(expand_x=xpandr, expand_y=xpandr)
|
|
621
|
+
window['-OUTPUT-'].update('')
|
|
622
|
+
#sg.easy_print(PREFDICT)
|
|
623
|
+
window.refresh()
|
|
624
|
+
|
|
625
|
+
if window['-OUTPUT-'].get():
|
|
626
|
+
#update menu. This is a PIA.
|
|
627
|
+
menulayout[2][1][2] = f'&Save Output to File {MOD}S::-SAVE-'
|
|
628
|
+
menulayout[2][1][3] = f'&Print Output {MOD}P::-PRINT-'
|
|
629
|
+
window['-MENUBAR-'].update(menulayout)
|
|
630
|
+
|
|
631
|
+
if event.endswith('-SAVE-'):
|
|
632
|
+
send_to_file(values['-OUTPUT-'])
|
|
633
|
+
|
|
634
|
+
if event.endswith('-PRINT-'):
|
|
635
|
+
send_to_printer(values['-OUTPUT-'])
|
|
636
|
+
else:
|
|
637
|
+
menulayout[2][1][2] = f'!&Save Output to File {MOD}S::-SAVE-'
|
|
638
|
+
menulayout[2][1][3] = f'!&Print Output {MOD}P::-PRINT-'
|
|
639
|
+
window['-MENUBAR-'].update(menulayout)
|
|
640
|
+
|
|
641
|
+
#Menubar events
|
|
642
|
+
if event == 'Add Files':
|
|
643
|
+
newfiles = sg.popup_get_file(message='', no_window=True,
|
|
644
|
+
multiple_files=True,
|
|
645
|
+
file_types = sg.FILE_TYPES_ALL_FILES)
|
|
646
|
+
|
|
647
|
+
upd_list = (window['-SELECT-'].get_list_values() +
|
|
648
|
+
[x for x in newfiles if
|
|
649
|
+
x not in window['-SELECT-'].get_list_values()])
|
|
650
|
+
window['-SELECT-'].update(upd_list)
|
|
651
|
+
|
|
652
|
+
if event == 'Credits and Details':
|
|
653
|
+
about_window()
|
|
654
|
+
if event == 'Damage Help':
|
|
655
|
+
webbrowser.open('https://ubc-library-rc.github.io/damage')
|
|
656
|
+
|
|
657
|
+
#Copypasta
|
|
658
|
+
if event.endswith(':-COPY-'):
|
|
659
|
+
#sg.clipboard_get()
|
|
660
|
+
#Oh look more straight Tkinter.
|
|
661
|
+
try:
|
|
662
|
+
sg.clipboard_set(window['-OUTPUT-'].Widget.selection_get())
|
|
663
|
+
except sg.tkinter.TclError:
|
|
664
|
+
pass
|
|
665
|
+
#sg.easy_print('COPY!!!', c='white on blue')
|
|
666
|
+
#sg.easy_print(sg.clipboard_get())
|
|
667
|
+
if event.endswith(':-PASTE-'):#Menu only because pastes by default
|
|
668
|
+
try:
|
|
669
|
+
#sg.easy_print('PASTE!!!', c='white on orange')
|
|
670
|
+
window['-OUTPUT-'].Widget.insert(sg.tk.INSERT,
|
|
671
|
+
sg.clipboard_get())
|
|
672
|
+
except sg.tkinter.TclError:
|
|
673
|
+
pass
|
|
674
|
+
|
|
675
|
+
window.close()
|
|
676
|
+
|
|
677
|
+
if __name__ == '__main__':
|
|
678
|
+
main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2021 University of British Columbia Library
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: damage
|
|
3
|
+
Version: 0.3.14
|
|
4
|
+
Summary: File manifest generator and python package for statistical data files and documentation'
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: metadata,SAS,SPSS,Stata,rectangular files,manifest generator
|
|
7
|
+
Author: Paul Lesack
|
|
8
|
+
Author-email: paul.lesack@ubc.ca
|
|
9
|
+
Requires-Python: >=3.12, <4
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Environment :: MacOS X
|
|
13
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
14
|
+
Classifier: Environment :: X11 Applications
|
|
15
|
+
Classifier: Intended Audience :: Education
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Education
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Dist: chardet (>=5.2.0,<6.0.0)
|
|
23
|
+
Requires-Dist: freesimplegui (>=5.2.0,<6.0.0)
|
|
24
|
+
Requires-Dist: numpy (>=2.2.3,<3.0.0)
|
|
25
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
26
|
+
Requires-Dist: pyreadstat (>=1.2.8,<2.0.0)
|
|
27
|
+
Project-URL: Homepage, https://ubc-library-rc.github.io/damage
|
|
28
|
+
Project-URL: Issue Tracker, https://github.com/ubc-library-rc/damage/issues
|
|
29
|
+
Project-URL: Repository, https://github.com/ubc-library-rc/damage
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# File manifest tools: Damage
|
|
33
|
+
|
|
34
|
+
Damage is a simple command-line utility which outputs a file manifest in a variety of formats, with a special focus on statistical package files from SPSS, SAS and Stata. It's also the name of the Python package which you can use in your own code and which powers the _damage_ utility.
|
|
35
|
+
|
|
36
|
+
Source code and documentation files are available at <https://github.com/ubc-library-rc/damage>. Documentation is in the intuitively named _docs_ subdirectory.
|
|
37
|
+
|
|
38
|
+
Binary versions of the *damage* utility for Windows and MacOS computers can be found on the project's Github release page: <https://github.com/ubc-library-rc/damage/releases>.
|
|
39
|
+
|
|
40
|
+
A less utilitarian documentation viewing experience is available at <https://ubc-library-rc.github.io/damage/>.
|
|
41
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
damage/__init__.py,sha256=HTjQQenVmw0ilI8WHQb1mISvx-NWtUvicKX4xezMSWw,14872
|
|
2
|
+
damage/console/damage_cmd.py,sha256=5CPh_HpbDRSuUybxvGDLxAcW6Aec4HW5hJeMAsb5PU4,5079
|
|
3
|
+
damage/gui/assets/DamageAppIcon.icns,sha256=_ObyYd-c8D7zlQosQnOVqv12zteMzIykzzZkmkZg3Mk,91447
|
|
4
|
+
damage/gui/assets/DamageAppIcon.ico,sha256=tnf5VUkYXV27R34DhM0GqBXkbMoz2DwbCD9vM9vpzxM,622678
|
|
5
|
+
damage/gui/assets/DamageAppIcon.jpg,sha256=kBEFy52EUWlLY3HDphQSFd2wIUHAEbT750E5HassNUk,76307
|
|
6
|
+
damage/gui/assets/DamageAppIcon.png,sha256=E0HIRi8i7Isa3vCr7pT6fVXQ9qHgkzuOs_ZJ52HQTao,52486
|
|
7
|
+
damage/gui/assets/LICENCE.txt,sha256=W3g7rf2LtjFk4Fhp740RSuB_cHgVR5p2Gq61JyufvmI,1092
|
|
8
|
+
damage/gui/damage_gui.py,sha256=0X3dkfhSVpmFQZLhiNYP33jJ9WnUuebm3LkRR5-Mr-g,27198
|
|
9
|
+
damage-0.3.14.dist-info/LICENCE.md,sha256=oVoR07zgulhKIi0J7RA518_QhHqqfdq9p9B8aYCMVd8,1091
|
|
10
|
+
damage-0.3.14.dist-info/METADATA,sha256=T4YBN0J-dVFcEYyT3qoWZ_ZIyd7mNu93D5AOJkLa6yU,2045
|
|
11
|
+
damage-0.3.14.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
12
|
+
damage-0.3.14.dist-info/entry_points.txt,sha256=K-F6BbGVbvPIRvbQCCE-TZWDymgmGgFUmaQG5UP1QuI,148
|
|
13
|
+
damage-0.3.14.dist-info/RECORD,,
|