skilleter-thingy 0.0.37__py3-none-any.whl → 0.0.39__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.

Files changed (68) hide show
  1. {skilleter_thingy-0.0.37.dist-info → skilleter_thingy-0.0.39.dist-info}/METADATA +1 -1
  2. skilleter_thingy-0.0.39.dist-info/RECORD +6 -0
  3. skilleter_thingy-0.0.39.dist-info/top_level.txt +1 -0
  4. skilleter_thingy/__init__.py +0 -6
  5. skilleter_thingy/addpath.py +0 -107
  6. skilleter_thingy/borger.py +0 -269
  7. skilleter_thingy/console_colours.py +0 -63
  8. skilleter_thingy/diskspacecheck.py +0 -67
  9. skilleter_thingy/docker_purge.py +0 -113
  10. skilleter_thingy/ffind.py +0 -536
  11. skilleter_thingy/ggit.py +0 -90
  12. skilleter_thingy/ggrep.py +0 -154
  13. skilleter_thingy/git_br.py +0 -180
  14. skilleter_thingy/git_ca.py +0 -142
  15. skilleter_thingy/git_cleanup.py +0 -287
  16. skilleter_thingy/git_co.py +0 -220
  17. skilleter_thingy/git_common.py +0 -61
  18. skilleter_thingy/git_hold.py +0 -154
  19. skilleter_thingy/git_mr.py +0 -92
  20. skilleter_thingy/git_parent.py +0 -77
  21. skilleter_thingy/git_review.py +0 -1428
  22. skilleter_thingy/git_update.py +0 -385
  23. skilleter_thingy/git_wt.py +0 -96
  24. skilleter_thingy/gitcmp_helper.py +0 -322
  25. skilleter_thingy/gitprompt.py +0 -274
  26. skilleter_thingy/gl.py +0 -174
  27. skilleter_thingy/gphotosync.py +0 -610
  28. skilleter_thingy/linecount.py +0 -155
  29. skilleter_thingy/moviemover.py +0 -133
  30. skilleter_thingy/photodupe.py +0 -136
  31. skilleter_thingy/phototidier.py +0 -248
  32. skilleter_thingy/py_audit.py +0 -131
  33. skilleter_thingy/readable.py +0 -270
  34. skilleter_thingy/remdir.py +0 -126
  35. skilleter_thingy/rmdupe.py +0 -550
  36. skilleter_thingy/rpylint.py +0 -91
  37. skilleter_thingy/splitpics.py +0 -99
  38. skilleter_thingy/strreplace.py +0 -82
  39. skilleter_thingy/sysmon.py +0 -435
  40. skilleter_thingy/tfm.py +0 -920
  41. skilleter_thingy/tfparse.py +0 -101
  42. skilleter_thingy/thingy/__init__.py +0 -0
  43. skilleter_thingy/thingy/colour.py +0 -213
  44. skilleter_thingy/thingy/dc_curses.py +0 -278
  45. skilleter_thingy/thingy/dc_defaults.py +0 -221
  46. skilleter_thingy/thingy/dc_util.py +0 -50
  47. skilleter_thingy/thingy/dircolors.py +0 -308
  48. skilleter_thingy/thingy/docker.py +0 -95
  49. skilleter_thingy/thingy/files.py +0 -142
  50. skilleter_thingy/thingy/git.py +0 -1371
  51. skilleter_thingy/thingy/git2.py +0 -1307
  52. skilleter_thingy/thingy/gitlab.py +0 -193
  53. skilleter_thingy/thingy/logger.py +0 -112
  54. skilleter_thingy/thingy/path.py +0 -156
  55. skilleter_thingy/thingy/popup.py +0 -87
  56. skilleter_thingy/thingy/process.py +0 -112
  57. skilleter_thingy/thingy/run.py +0 -334
  58. skilleter_thingy/thingy/tfm_pane.py +0 -595
  59. skilleter_thingy/thingy/tidy.py +0 -160
  60. skilleter_thingy/trimpath.py +0 -84
  61. skilleter_thingy/window_rename.py +0 -92
  62. skilleter_thingy/xchmod.py +0 -125
  63. skilleter_thingy/yamlcheck.py +0 -89
  64. skilleter_thingy-0.0.37.dist-info/RECORD +0 -66
  65. skilleter_thingy-0.0.37.dist-info/top_level.txt +0 -1
  66. {skilleter_thingy-0.0.37.dist-info → skilleter_thingy-0.0.39.dist-info}/LICENSE +0 -0
  67. {skilleter_thingy-0.0.37.dist-info → skilleter_thingy-0.0.39.dist-info}/WHEEL +0 -0
  68. {skilleter_thingy-0.0.37.dist-info → skilleter_thingy-0.0.39.dist-info}/entry_points.txt +0 -0
@@ -1,550 +0,0 @@
1
- #! /usr/bin/env python3
2
-
3
- ################################################################################
4
- """ Find duplicate files and do things with them.
5
-
6
- Uses the 'jdupes' utility
7
-
8
- TODO: Option to ignore by filetype
9
- TODO: Ignore folder.jpg files
10
-
11
- NOTE: The option to ignore directories in jdupes doesn't work (at least in the version in Ubuntu 18.04) so we do this after searching for duplicates
12
- """
13
- ################################################################################
14
-
15
- import os
16
- import argparse
17
- import logging
18
- import subprocess
19
- import sys
20
- import re
21
- import pickle
22
- import copy
23
- import fnmatch
24
-
25
- ################################################################################
26
-
27
- ALWAYS_IGNORE_DIRS = ['.git']
28
-
29
- ################################################################################
30
-
31
- def error(msg):
32
- """ Report an error and exit """
33
-
34
- sys.stderr.write('%s\n' % msg)
35
- sys.exit(1)
36
-
37
- ################################################################################
38
-
39
- def parse_command_line():
40
- """ Parse the command line """
41
-
42
- parser = argparse.ArgumentParser(description='Find duplicate files created by SyncThing or in temporary directories in a given path')
43
- parser.add_argument('--debug', action='store_true', help='Debug output')
44
- parser.add_argument('--save', action='store', help='Save duplicate file list')
45
- parser.add_argument('--load', action='store', help='Load duplicate file list')
46
- parser.add_argument('--script', action='store', help='Generate a shell script to delete the duplicates')
47
- parser.add_argument('--exclude', action='append', help='Directories to skip when looking for duplicates')
48
- parser.add_argument('--ignore', action='append', help='Wildcards to ignore when looking for duplicates')
49
- parser.add_argument('path', nargs='?', default='.', help='Path(s) to search for duplicates')
50
-
51
- args = parser.parse_args()
52
-
53
- logging.basicConfig(level=logging.DEBUG if args.debug else logging.ERROR)
54
-
55
- if args.save and args.load:
56
- error('The save and load options are mutually exclusive')
57
-
58
- return args
59
-
60
- ################################################################################
61
-
62
- def jdupes(path,
63
- one_file_system=False,
64
- no_hidden=False,
65
- check_permissions=False,
66
- quick=False,
67
- recurse=True,
68
- follow_symlinks=False,
69
- exclude=None,
70
- zero_match=False):
71
- """ Run jdupes with the specified options """
72
-
73
- cmd = ['jdupes', '--quiet']
74
-
75
- if one_file_system:
76
- cmd.append('--one-file-system')
77
-
78
- if no_hidden:
79
- cmd.append('--nohidden')
80
-
81
- if check_permissions:
82
- cmd.append('--permissions')
83
-
84
- if quick:
85
- cmd.append('--quick')
86
-
87
- if recurse:
88
- cmd += ['--recurse', path]
89
- else:
90
- cmd.append(path)
91
-
92
- if follow_symlinks:
93
- cmd.append('--symlinks')
94
-
95
- if exclude:
96
- cmd += ['--exclude', exclude]
97
-
98
- if zero_match:
99
- cmd.append('--zeromatch')
100
-
101
- logging.debug('Running %s', ' '.join(cmd))
102
-
103
- result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
104
-
105
- results = [[]]
106
- for output in result.stdout.split('\n'):
107
- output = output.strip()
108
-
109
- logging.debug(output)
110
-
111
- if output:
112
- results[-1].append(output)
113
- else:
114
- results.append([])
115
-
116
- while results and results[-1] == []:
117
- results = results[:-1]
118
-
119
- logging.debug('Found %d duplicated files', len(results))
120
- for entry in results:
121
- logging.debug(' %s', ', '.join(entry))
122
-
123
- return results
124
-
125
- ################################################################################
126
-
127
- def remove_excluded_entries(args, duplicates):
128
- """ Now filter out enties in the duplicates lists that are in the
129
- directories that we are supposed to be ignoring """
130
-
131
- # Build the list of directories to ignore - add the default list
132
-
133
- ignore_dirs = ALWAYS_IGNORE_DIRS
134
- if args.exclude:
135
- ignore_dirs += args.exclude
136
-
137
- # Build the list of absolute and relative paths to ignore
138
- # both are in the form '/path/'
139
-
140
- ignore_prefixes = []
141
- ignore_subdirs = []
142
-
143
- for ignore in ignore_dirs:
144
- if ignore[-1] != '/':
145
- ignore = '%s/' % ignore
146
-
147
- if ignore[0] == '/':
148
- ignore_prefixes.append(ignore)
149
- else:
150
- ignore_subdirs.append('/%s' % ignore)
151
-
152
- # Now remove entries from the duplicate list that are within the ignored
153
- # directories. If the resultant duplicate record is empty or only contains
154
- # one entry, then remove it.
155
-
156
- filtered_duplicates = []
157
-
158
- for duplicate in duplicates:
159
- # Set of entries in the record to remove
160
-
161
- remove_entries = set()
162
-
163
- for entry in duplicate:
164
- # If the entry is in an excluded directory tree, remove it
165
-
166
- for ignore in ignore_prefixes:
167
- if entry.startswith(ignore):
168
- remove_entries.add(entry)
169
-
170
- # If the entry is in an excluded subdirectory tree, remove it
171
-
172
- for ignore in ignore_subdirs:
173
- if ignore in entry:
174
- remove_entries.add(entry)
175
-
176
- # If we have a list of files to ignore then check each entry against the list
177
- # and remove any matches.
178
-
179
- for ignore in args.ignore or []:
180
- if fnmatch.fnmatch(os.path.basename(entry), ignore):
181
- remove_entries.add(entry)
182
-
183
- # If we loaded a saved list and the entry doesn't exist, remove it
184
-
185
- if args.load and entry not in remove_entries and not os.path.isfile(entry):
186
- remove_entries.add(entry)
187
-
188
- # If we have entries to remove from the record, then remove them
189
-
190
- if remove_entries:
191
- for entry in remove_entries:
192
- duplicate.remove(entry)
193
-
194
- # Only add to the filtered duplicate list if we have more than one duplicate in the entry
195
-
196
- if len(duplicate) >= 2:
197
- filtered_duplicates.append(duplicate)
198
-
199
- return filtered_duplicates
200
-
201
- ################################################################################
202
-
203
- def find_duplicates(args):
204
- """ Find duplicates, or load them from a saved status file """
205
-
206
- if args.load:
207
- logging.debug('Loading duplicate file data from %s', args.load)
208
-
209
- with open(args.load, 'rb') as infile:
210
- duplicates = pickle.load(infile)
211
-
212
- logging.debug('Data loaded, %d duplicates', len(duplicates))
213
- else:
214
- duplicates = jdupes(args.path)
215
-
216
- if args.save:
217
- logging.debug('Saving duplicate file data to %s', args.save)
218
-
219
- with open(args.save, 'wb') as outfile:
220
- pickle.dump(duplicates, outfile)
221
-
222
- print('Duplicate file data saved')
223
- sys.exit(0)
224
-
225
- return remove_excluded_entries(args, duplicates)
226
-
227
- ################################################################################
228
-
229
- def check_duplicates(duplicate):
230
- """ Given a list of duplicate files work out what to do with them.
231
- Returns:
232
- List of files (if any) to keep
233
- List of files (if any) to be deleted
234
- Name of a file that is similar to the duplicates (both in name and content)
235
- True if the files being removed are .ini files with mangled names
236
- Any error/warning message associated with processing the duplicates
237
- """
238
-
239
- keep = set()
240
- remove = set()
241
- similar = None
242
- error_msg = None
243
-
244
- # We can just delete entries that are conflicting picasa.ini files
245
-
246
- for entry in duplicate:
247
- if re.fullmatch(r'.*/\.?[pP]icasa.sync-conflict-.*\.ini', entry):
248
- logging.debug('Remove picasa.ini sync conflict: %s', entry)
249
-
250
- remove.add(entry)
251
-
252
- if remove:
253
- for item in remove:
254
- duplicate.remove(item)
255
-
256
- ini_file_purge = (len(remove) > 0)
257
-
258
- # If all of the files are called 'picasa.ini' then we skip them as it is valid to have multiple picasa.ini files
259
-
260
- if duplicate:
261
- for entry in duplicate:
262
- if os.path.basename(entry).lower() not in ('picasa.ini', '.picasa.ini'):
263
- break
264
- else:
265
- print('Keeping picasa.ini files: %s' % (', '.join(duplicate)))
266
- duplicate = []
267
-
268
- # Skip other checks if we don't have any files that aren't conflicting picasa.ini files
269
-
270
- if duplicate:
271
- # Look for entries that are in known temporary directories
272
-
273
- for entry in duplicate:
274
- if re.match(r'.*/(\$RECYCLE\.BIN|.Picasa3Temp|.Picasa3Temp_[0-9]+|.picasaoriginals)/.*', entry):
275
- logging.debug('Removing temporary directory item: %s', entry)
276
- remove.add(entry)
277
- else:
278
- keep.add(entry)
279
-
280
- # Look for lists of copies where some are marked as copies with _X appended to the file name
281
-
282
- if len(keep) > 1:
283
- copies = set()
284
- originals = set()
285
-
286
- for entry in keep:
287
- if re.fullmatch(r'.*_[1-9][0-9]{0,2}\.[^/]+', entry):
288
- copies.add(entry)
289
- else:
290
- originals.add(entry)
291
-
292
- # If we have at least one original, then we can remove the copies
293
-
294
- if originals:
295
- if copies:
296
- logging.debug('Removing copies: %s', list(copies))
297
- logging.debug('Keeping originals: %s', originals)
298
-
299
- remove |= copies
300
- keep = originals
301
- else:
302
- error_msg = 'No originals found in %s' % (', '.join(keep))
303
-
304
- # Looks for lists of copies where some are marked as copies with (N) appended to the file name
305
-
306
- copies = set()
307
- originals = set()
308
-
309
- for entry in keep:
310
- if re.fullmatch(r'.*\([0-9]+\)\.[^/]+', entry):
311
- copies.add(entry)
312
- else:
313
- originals.add(entry)
314
-
315
- # If we have at least one original, then we can remove the copies
316
-
317
- if originals:
318
- if copies:
319
- logging.debug('Removing copies: %s', list(copies))
320
- logging.debug('Keeping originals: %s', originals)
321
-
322
- remove |= copies
323
- keep = originals
324
- else:
325
- error_msg = 'No originals found in %s' % (', '.join(keep))
326
-
327
- # Now look for sync conflicts
328
-
329
- if len(keep) > 1:
330
- conflicts = set()
331
-
332
- for entry in keep:
333
- if re.fullmatch(r'.*(\.sync-conflict-|/.stversions/).*', entry):
334
- conflicts.add(entry)
335
-
336
- if conflicts:
337
- keep = keep.difference(conflicts)
338
-
339
- if keep:
340
- logging.debug('Removing sync conflicts: %s', conflicts)
341
- logging.debug('Keeping: %s', keep)
342
-
343
- remove |= conflicts
344
- else:
345
- logging.debug('No non-conflicting files found in %s', (', '.join(conflicts)))
346
-
347
- originals = set()
348
-
349
- for entry in conflicts:
350
- originals.add(re.sub(r'(\.sync-conflict-[0-9]{8}-[0-9]{6}-[A-Z]{7}|/.stversions/)', '', entry))
351
-
352
- if len(originals) == 1:
353
- original = originals.pop()
354
- if os.path.isfile(original):
355
-
356
- similar = original
357
- remove = conflicts
358
-
359
- # Now look for files that differ only by case
360
-
361
- if len(keep) > 1:
362
- # Take a copy of the set, then compare the lower case versions of the entries
363
- # and remove any that match
364
- # TODO: We only check for a match against a lower case version of the first entry
365
-
366
- keep_c = copy.copy(keep)
367
- name_lc = keep_c.pop().lower()
368
-
369
- for entry in keep_c:
370
- if entry.lower() == name_lc:
371
- logging.debug('Removing duplicate mixed-case entry: %s', entry)
372
-
373
- remove.add(entry)
374
-
375
- keep = keep.difference(remove)
376
-
377
- # Now look for files with '~' in the name
378
-
379
- if len(keep) > 1:
380
- tilde = set()
381
-
382
- for k in keep:
383
- if '~' in k:
384
- tilde.add(k)
385
-
386
- if tilde != keep:
387
- remove |= tilde
388
- keep = keep.difference(tilde)
389
-
390
- # Now remove entries with the shorter subdirectory names
391
-
392
- if len(keep) > 1:
393
- longest = ""
394
- longest_name = None
395
-
396
- for k in sorted(list(keep)):
397
- subdir = os.path.split(os.path.dirname(k))[1]
398
-
399
- if len(subdir) > len(longest):
400
- longest = subdir
401
- longest_name = k
402
-
403
- if longest_name:
404
- for k in keep:
405
- if k != longest_name:
406
- remove.add(k)
407
-
408
- keep = keep.difference(remove)
409
-
410
- # Now remove entries with the shorter file names
411
-
412
- if len(keep) > 1:
413
- longest = ""
414
- longest_name = None
415
-
416
- for k in sorted(list(keep)):
417
- filename = os.path.basename(k)
418
-
419
- if len(filename) > len(longest):
420
- longest = filename
421
- longest_name = k
422
-
423
- if longest_name:
424
- for k in keep:
425
- if k != filename:
426
- remove.add(k)
427
-
428
- keep = keep.difference(remove)
429
-
430
- # Don't allow files called 'folder.jpg' to be removed - multiple directories can
431
- # have the same cover art.
432
-
433
- if remove:
434
- for r in remove:
435
- if os.path.basename(r) in ('folder.jpg', 'Folder.jpg', 'cover.jpg', 'Cover.jpg'):
436
- keep.add(r)
437
-
438
- remove = remove.difference(keep)
439
-
440
- return sorted(list(keep)), sorted(list(remove)), similar, ini_file_purge, error_msg
441
-
442
- ################################################################################
443
-
444
- def process_duplicates(args, duplicates):
445
- """ Process the duplicate file records """
446
-
447
- # Optionally generate the shell script
448
-
449
- if args.script:
450
- script = open(args.script, 'wt')
451
-
452
- script.write('#! /usr/bin/env bash\n\n'
453
- '# Auto-generated shell script to delete duplicate files\n\n'
454
- 'set -o pipefail\n'
455
- 'set -o errexit\n'
456
- 'set -o nounset\n\n')
457
-
458
- # List of errors - we report everything that doesn't work at the end
459
-
460
- errors = []
461
-
462
- # Decide what to do with each duplication record
463
-
464
- for duplicate in duplicates:
465
- keep, remove, similar, ini_file_purge, error_msg = check_duplicates(duplicate)
466
-
467
- if error_msg:
468
- errors.append(error_msg)
469
-
470
- # Report what we'd do
471
-
472
- if args.script and (remove or keep):
473
- script.write('\n')
474
-
475
- for k in keep:
476
- script.write('# Keep %s\n' % k)
477
-
478
- if ini_file_purge:
479
- script.write('# Remove conflicting, renamed picasa.ini files\n')
480
-
481
- if similar:
482
- script.write('# Similar file: %s\n' % similar)
483
-
484
- for r in remove:
485
- r = r.replace('$', '\\$')
486
- script.write('rm -- "%s"\n' % r)
487
-
488
- if remove:
489
- print('Duplicates found:')
490
-
491
- if keep:
492
- print(' Keep: %s' % (', '.join(keep)))
493
-
494
- if similar:
495
- print(' Similar: %s' % similar)
496
-
497
- print(' Delete: %s' % (', '.join(remove)))
498
-
499
- elif keep and not remove:
500
- errors.append('Keeping all copies of %s' % (', '.join(keep)))
501
-
502
- elif len(keep) > 1:
503
- print('Keeping %d copies of %s' % (len(keep), ', '.join(keep)))
504
- print(' Whilst removing %s' % (', '.join(remove)))
505
-
506
- elif duplicate and remove and not keep:
507
- errors.append('All entries classified for removal: %s' % (', '.join(remove)))
508
-
509
- if errors:
510
- errors.sort()
511
-
512
- print('-' * 80)
513
- print('Problems:')
514
-
515
- for error in errors:
516
- print(error)
517
-
518
- if args.script:
519
- script.write('\n'
520
- '# %s\n'
521
- '# There are a number of duplicates where it is not clear which one should be kept,\n'
522
- '# or whether all copies should be kept. These are listed below.\n'
523
- '# %s\n\n' % ('-' * 80, '-' * 80))
524
-
525
- for error in errors:
526
- script.write('# %s\n' % error)
527
-
528
- ################################################################################
529
-
530
- def rmdupe():
531
- """ Main function """
532
-
533
- try:
534
- args = parse_command_line()
535
-
536
- duplicates = find_duplicates(args)
537
-
538
- process_duplicates(args, duplicates)
539
-
540
- except KeyboardInterrupt:
541
- sys.exit(1)
542
-
543
- except BrokenPipeError:
544
- sys.exit(2)
545
-
546
- ################################################################################
547
- # Entry point
548
-
549
- if __name__ == '__main__':
550
- rmdupe()
@@ -1,91 +0,0 @@
1
- #! /usr/bin/env python3
2
-
3
- ################################################################################
4
- """ Run pylint on all the Python source files in the current tree
5
-
6
- Copyright (C) 2017-18 John Skilleter """
7
- ################################################################################
8
-
9
- import os
10
- import sys
11
- import argparse
12
- import glob
13
-
14
- # TODO: Convert to use thingy.proc
15
- import thingy.process as process
16
-
17
- ################################################################################
18
-
19
- def main():
20
- """ Main code. Exits directly on failure to locate source files, or returns
21
- the status code from Pylint otherwise. """
22
-
23
- # Parse the comand line
24
-
25
- parser = argparse.ArgumentParser(description='Run pylint in the current (or specified) directory/ies')
26
-
27
- parser.add_argument('paths', nargs='*', help='List of files or paths to lint')
28
-
29
- args = parser.parse_args()
30
-
31
- if not args.paths:
32
- args.paths = ['.']
33
-
34
- sourcefiles = []
35
-
36
- # Use rgrep to find source files that have a Python 3 #!
37
-
38
- for entry in args.paths:
39
- if os.path.isdir(entry):
40
- try:
41
- sourcefiles += process.run(['rgrep', '-E', '--exclude-dir=.git', '-l', '#![[:space:]]*/usr/bin/(env[[:space:]])?python3'] + args.paths)
42
- except process.RunError as exc:
43
- if exc.status == 1:
44
- sys.stderr.write('No Python3 source files found\n')
45
- sys.exit(2)
46
- else:
47
- sys.stderr.write('%d: %s\n' % (exc.status, exc.msg))
48
- sys.exit(1)
49
- elif os.path.isfile(entry):
50
- sourcefiles.append(entry)
51
- else:
52
- files = glob.glob(entry)
53
-
54
- if not files:
55
- sys.stderr.write('No files found matching "%s"' % entry)
56
- sys.exit(2)
57
-
58
- sourcefiles += files
59
-
60
- # Run pylint on all the files
61
-
62
- try:
63
- process.run(['pylint3', '--output-format', 'parseable'] + sourcefiles, foreground=True)
64
- except process.RunError as exc:
65
- status = exc.status
66
- else:
67
- status = 0
68
-
69
- if status >= 64:
70
- sys.stderr.write('Unexpected error: %d\n' % status)
71
-
72
- # Function return code is the status return from pylint
73
-
74
- return status
75
-
76
- ################################################################################
77
-
78
- def rpylint():
79
- """Entry point"""
80
-
81
- try:
82
- sys.exit(main())
83
- except KeyboardInterrupt:
84
- sys.exit(1)
85
- except BrokenPipeError:
86
- sys.exit(2)
87
-
88
- ################################################################################
89
-
90
- if __name__ == '__main__':
91
- rpylint()