skilleter-thingy 0.1.19__tar.gz → 0.1.21__tar.gz

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 (77) hide show
  1. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/PKG-INFO +1 -1
  2. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/pyproject.toml +1 -1
  3. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/ggrep.py +2 -1
  4. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_br.py +1 -1
  5. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_parent.py +2 -1
  6. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_review.py +2 -2
  7. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_update.py +2 -2
  8. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/gphotosync.py +9 -5
  9. skilleter_thingy-0.1.21/skilleter_thingy/localphotosync.py +190 -0
  10. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/multigit.py +1 -1
  11. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/photodupe.py +10 -10
  12. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/py_audit.py +4 -2
  13. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/readable.py +13 -11
  14. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/sysmon.py +2 -2
  15. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/colour.py +1 -1
  16. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/dircolors.py +20 -18
  17. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/git2.py +0 -1
  18. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/popup.py +1 -1
  19. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/tfm_pane.py +3 -3
  20. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/tidy.py +14 -13
  21. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/venv_create.py +1 -1
  22. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy.egg-info/PKG-INFO +1 -1
  23. skilleter_thingy-0.1.21/skilleter_thingy.egg-info/PKG-INFO 2 +193 -0
  24. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy.egg-info/SOURCES.txt +1 -0
  25. skilleter_thingy-0.1.19/skilleter_thingy/localphotosync.py +0 -471
  26. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/LICENSE +0 -0
  27. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/README.md +0 -0
  28. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/setup.cfg +0 -0
  29. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/__init__.py +0 -0
  30. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/addpath.py +0 -0
  31. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/borger.py +0 -0
  32. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/console_colours.py +0 -0
  33. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/diskspacecheck.py +0 -0
  34. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/docker_purge.py +0 -0
  35. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/ffind.py +0 -0
  36. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/ggit.py +0 -0
  37. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_ca.py +0 -0
  38. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_cleanup.py +0 -0
  39. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_co.py +0 -0
  40. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_common.py +0 -0
  41. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_hold.py +0 -0
  42. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_mr.py +0 -0
  43. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_retag.py +0 -0
  44. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/git_wt.py +0 -0
  45. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/gitcmp_helper.py +0 -0
  46. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/gitprompt.py +0 -0
  47. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/gl.py +0 -0
  48. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/linecount.py +0 -0
  49. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/moviemover.py +0 -0
  50. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/phototidier.py +0 -0
  51. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/remdir.py +0 -0
  52. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/rmdupe.py +0 -0
  53. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/rpylint.py +0 -0
  54. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/splitpics.py +0 -0
  55. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/strreplace.py +0 -0
  56. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/tfm.py +0 -0
  57. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/tfparse.py +0 -0
  58. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/__init__.py +0 -0
  59. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/dc_curses.py +0 -0
  60. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/dc_defaults.py +0 -0
  61. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/dc_util.py +0 -0
  62. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/docker.py +0 -0
  63. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/files.py +0 -0
  64. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/git.py +0 -0
  65. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/gitlab.py +0 -0
  66. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/path.py +0 -0
  67. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/process.py +0 -0
  68. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/run.py +0 -0
  69. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/thingy/venv_template.py +0 -0
  70. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/trimpath.py +0 -0
  71. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/window_rename.py +0 -0
  72. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/xchmod.py +0 -0
  73. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy/yamlcheck.py +0 -0
  74. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy.egg-info/dependency_links.txt +0 -0
  75. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy.egg-info/entry_points.txt +0 -0
  76. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy.egg-info/requires.txt +0 -0
  77. {skilleter_thingy-0.1.19 → skilleter_thingy-0.1.21}/skilleter_thingy.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skilleter_thingy
3
- Version: 0.1.19
3
+ Version: 0.1.21
4
4
  Summary: A collection of useful utilities, mainly aimed at making Git more friendly
5
5
  Author-email: John Skilleter <john@skilleter.org.uk>
6
6
  Project-URL: Home, https://skilleter.org.uk
@@ -7,7 +7,7 @@ name = "skilleter_thingy"
7
7
 
8
8
  # Version must be incremented to install updated Thingy
9
9
 
10
- version = "0.1.19"
10
+ version = "0.1.21"
11
11
 
12
12
  authors = [
13
13
  {name="John Skilleter", email="john@skilleter.org.uk"},
@@ -39,7 +39,8 @@ def parse_command_line():
39
39
  parser.add_argument('--files-with-matches', '-l', action='store_true', help='Show only the names of files that contain matches')
40
40
  parser.add_argument('--files-without-matches', '-L', action='store_true', help='Show only the names of files that do NOT contain matches')
41
41
  parser.add_argument('--wildcard', '-W', action='append', help='Only search files matching the wildcard(s)')
42
- parser.add_argument('--only-matching', '-o', action='store_true', help='Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line.')
42
+ parser.add_argument('--only-matching', '-o', action='store_true',
43
+ help='Print only the matched (non-empty) parts of a matching line, with each such part on a separate output line.')
43
44
  parser.add_argument('--no-color', action='store_true', help='Turn off match highlighting')
44
45
  parser.add_argument('pattern', action='store', help='Regular expression to search for')
45
46
  parser.add_argument('paths', nargs='*', help='Optional list of one or more paths to search')
@@ -105,7 +105,7 @@ def list_branches(branches):
105
105
  output = []
106
106
 
107
107
  for i, field in enumerate(branch):
108
- if i==1:
108
+ if i == 1:
109
109
  field = parse(field)
110
110
  time_str = field.strftime('%H:%M:%S')
111
111
 
@@ -23,7 +23,8 @@ def main():
23
23
  parser = argparse.ArgumentParser(description='Attempt to determine the parent branch for the specified branch (defaulting to the current one)')
24
24
  parser.add_argument('--all', '-a', action='store_true', help='Include feature branches as possible parents')
25
25
  parser.add_argument('--verbose', '-v', action='store_true', help='Report verbose results (includes number of commits between branch and parent)')
26
- parser.add_argument('branch', action='store', nargs='?', type=str, default=current_branch, help=f'Branch, commit or commit (defaults to current branch; {current_branch})')
26
+ parser.add_argument('branch', action='store', nargs='?', type=str, default=current_branch,
27
+ help=f'Branch, commit or commit (defaults to current branch; {current_branch})')
27
28
 
28
29
  args = parser.parse_args()
29
30
 
@@ -448,7 +448,7 @@ class GitReview():
448
448
  self.filter_modified = pickle_data.get('filter_modified', self.filter_modified)
449
449
  self.sort_order = pickle_data.get('sort_order', self.sort_order)
450
450
  self.reverse_sort = pickle_data.get('reverse_sort', self.reverse_sort)
451
- self.filter_none_whitespace_only= pickle_data.get('filter_none_whitespace_only', self.filter_none_whitespace_only)
451
+ self.filter_none_whitespace_only = pickle_data.get('filter_none_whitespace_only', self.filter_none_whitespace_only)
452
452
  self.show_none_whitespace_stats = pickle_data.get('show_none_whitespace_stats', self.show_none_whitespace_stats)
453
453
 
454
454
  # Transfer the reviewed flag for each file in the pickle
@@ -460,7 +460,7 @@ class GitReview():
460
460
  newfile['reviewed'] = oldfile['reviewed']
461
461
  break
462
462
 
463
- except (EOFError, pickle.UnpicklingError, ModuleNotFoundError, AttributeError): # TODO: Why did I get ModuleNotFoundError or AttributeError????
463
+ except (EOFError, pickle.UnpicklingError, ModuleNotFoundError, AttributeError): # TODO: Why did I get ModuleNotFoundError or AttributeError????
464
464
  pass
465
465
 
466
466
  self.__constrain_display_parameters()
@@ -85,7 +85,7 @@ def branch_rebase(args, results, branch):
85
85
  if args.all_parents:
86
86
  parents, _ = git.parents()
87
87
  else:
88
- parents, _ = git.parents(ignore='feature/*' )
88
+ parents, _ = git.parents(ignore='feature/*')
89
89
 
90
90
  logging.debug('Probable parents of %s: %s', branch, parents)
91
91
 
@@ -314,7 +314,7 @@ def main():
314
314
 
315
315
  # List of stuff that's been done, to report in the summary
316
316
 
317
- results = {'deleted': set(), 'pulled': set(), 'failed': set(), 'rebased': set(), 'unchanged': set(), 'no-tracking': set() }
317
+ results = {'deleted': set(), 'pulled': set(), 'failed': set(), 'rebased': set(), 'unchanged': set(), 'no-tracking': set()}
318
318
 
319
319
  to_rebase = set()
320
320
 
@@ -111,15 +111,19 @@ def parse_command_line():
111
111
 
112
112
  parser.add_argument('--verbose', '-v', action='store_true', help='Output verbose status information')
113
113
  parser.add_argument('--dryrun', '-D', action='store_true', help='Just list files to be copied, without actually copying them')
114
- parser.add_argument('--picturedir', '-P', action='store', default=DEFAULT_PHOTO_DIR, help=f'Location of local picture storage directory (defaults to {DEFAULT_PHOTO_DIR})')
115
- parser.add_argument('--videodir', '-V', action='store', default=DEFAULT_VIDEO_DIR, help=f'Location of local video storage directory (defaults to {DEFAULT_VIDEO_DIR})')
114
+ parser.add_argument('--picturedir', '-P', action='store', default=DEFAULT_PHOTO_DIR,
115
+ help=f'Location of local picture storage directory (defaults to {DEFAULT_PHOTO_DIR})')
116
+ parser.add_argument('--videodir', '-V', action='store', default=DEFAULT_VIDEO_DIR,
117
+ help=f'Location of local video storage directory (defaults to {DEFAULT_VIDEO_DIR})')
116
118
  parser.add_argument('--start', '-s', action='store', default=None, help='Start date (in the form YYYY-MM, defaults to current month)')
117
119
  parser.add_argument('--end', '-e', action='store', default=None, help=f'End date (in the form YYYY-MM, defaults to {DEFAULT_MONTHS} before the start date)')
118
120
  parser.add_argument('--months', '-m', action='store', type=int, default=None, help='Synchronise this number of months of data (current month included)')
119
121
  parser.add_argument('--cache', '-c', action='store', default=DEFAULT_CACHE_DIR, help=f'Cache directory for Google photos (defaults to {DEFAULT_CACHE_DIR})')
120
- parser.add_argument('--rclone', '-r', action='store', default=DEFAULT_RCLONE_REMOTE, help=f'rclone remote name for Google photos (defaults to {DEFAULT_RCLONE_REMOTE})')
122
+ parser.add_argument('--rclone', '-r', action='store', default=DEFAULT_RCLONE_REMOTE,
123
+ help=f'rclone remote name for Google photos (defaults to {DEFAULT_RCLONE_REMOTE})')
121
124
  parser.add_argument('--no-update', '-N', action='store_true', help='Do not update local cache')
122
- parser.add_argument('--keep', '-k', action='store', type=int, default=DEFAULT_KEEP, help=f'Keep this number of months before the start date in the cache (defaults to {DEFAULT_KEEP})')
125
+ parser.add_argument('--keep', '-k', action='store', type=int, default=DEFAULT_KEEP,
126
+ help=f'Keep this number of months before the start date in the cache (defaults to {DEFAULT_KEEP})')
123
127
  parser.add_argument('--skip-no-day', '-z', action='store_true', help='Don\'t sync files where the day of the month could not be determined')
124
128
  parser.add_argument('action', nargs='*', help='Actions to perform (report or sync)')
125
129
 
@@ -450,7 +454,7 @@ def remove_duplicates(media_files):
450
454
  # Originals can have upper or lower case extensions, copies only tend to have lower
451
455
  # case, so build a lower case to original lookup table
452
456
 
453
- names = {name.lower():name for name in media_files}
457
+ names = {name.lower(): name for name in media_files}
454
458
 
455
459
  duplicates = defaultdict(list)
456
460
 
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Sync a directory tree full of photos into a tree organised by year, month and date
5
+ """
6
+
7
+ # TODO: Ignore patterns for source and destination file paths (.trashed* and .stversions)
8
+ # TODO: Use inotify to detect changes and run continuously
9
+
10
+ import os
11
+ import glob
12
+ import shutil
13
+ import sys
14
+ import logging
15
+ import argparse
16
+ import re
17
+
18
+ from enum import Enum
19
+
20
+ ################################################################################
21
+
22
+ # Default locations for local storage of photos and videos
23
+
24
+ DEFAULT_PHOTO_DIR = os.path.expanduser('~/Pictures')
25
+ DEFAULT_VIDEO_DIR = os.path.expanduser('~/Videos')
26
+
27
+ # File extensions (case-insensitive)
28
+
29
+ IMAGE_EXTENSIONS = ('.jpg', '.jpeg', '.png')
30
+ VIDEO_EXTENSIONS = ('.mp4', '.mov')
31
+
32
+ # Enum of filetypes
33
+
34
+ class FileType(Enum):
35
+ """File types"""
36
+ IMAGE = 0
37
+ VIDEO = 1
38
+ UNKNOWN = 2
39
+ IGNORE = 3
40
+
41
+ ################################################################################
42
+
43
+ def error(msg, status=1):
44
+ """Exit with an error message"""
45
+
46
+ print(msg)
47
+ sys.exit(status)
48
+
49
+ ################################################################################
50
+
51
+ def parse_command_line():
52
+ """Parse and validate the command line options"""
53
+
54
+ parser = argparse.ArgumentParser(description='Sync photos from Google Photos')
55
+
56
+ parser.add_argument('--verbose', '-v', action='store_true', help='Output verbose status information')
57
+ parser.add_argument('--dryrun', '--dry-run', '-D', action='store_true', help='Just list files to be copied, without actually copying them')
58
+ parser.add_argument('--picturedir', '-P', action='store', default=DEFAULT_PHOTO_DIR,
59
+ help=f'Location of local picture storage directory (defaults to {DEFAULT_PHOTO_DIR})')
60
+ parser.add_argument('--videodir', '-V', action='store', default=DEFAULT_VIDEO_DIR,
61
+ help=f'Location of local video storage directory (defaults to {DEFAULT_VIDEO_DIR})')
62
+ parser.add_argument('--path', '-p', action='store', default=None, help='Path to sync from')
63
+
64
+ args = parser.parse_args()
65
+
66
+ if not args.path:
67
+ error('You must specify a source directory')
68
+
69
+ # Configure debugging
70
+
71
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
72
+
73
+ # Report parameters if verbose
74
+
75
+ logging.debug('Source: %s', args.path)
76
+ logging.debug('Pictures: %s', args.picturedir)
77
+ logging.debug('Videos: %s', args.videodir)
78
+ logging.debug('Dry run: %d', args.dryrun)
79
+
80
+ return args
81
+
82
+ ################################################################################
83
+
84
+ def get_filetype(filename):
85
+ """Return the type of a file"""
86
+
87
+ _, ext = os.path.splitext(filename)
88
+
89
+ ext = ext.lower()
90
+
91
+ if ext in IMAGE_EXTENSIONS:
92
+ return FileType.IMAGE
93
+
94
+ if ext in VIDEO_EXTENSIONS:
95
+ return FileType.VIDEO
96
+
97
+ return FileType.UNKNOWN
98
+
99
+ ################################################################################
100
+
101
+ def media_sync(args):
102
+ """Sync photos and videos from args.path to date-structured directory
103
+ trees in args.picturedir and args.videodir.
104
+ Assumes that the source files are in Android naming format:
105
+ (IMG|VID)_YYYYMMDD_*.(jpg|mp4)
106
+ Looks for a destination directory called:
107
+ YYYY/YYYY-MM-DD*/
108
+ If multiple destination directories exist, it uses the first one when the
109
+ names are sorted alphbetically
110
+ If a file with the same name exists in the destination directory it is
111
+ not overwritten"""
112
+
113
+ files_copied = 0
114
+
115
+ filetype_re = re.compile(r'(PANO|IMG|VID)[-_](\d{4})(\d{2})(\d{2})[-_.].*')
116
+
117
+ for sourcefile in [source for source in glob.glob(os.path.join(args.path, '*')) if os.path.isfile(source)]:
118
+ filetype = get_filetype(sourcefile)
119
+
120
+ if filetype == FileType.IMAGE:
121
+ dest_dir = args.picturedir
122
+ elif filetype == FileType.VIDEO:
123
+ dest_dir = args.videodir
124
+ else:
125
+ logging.info('Ignoring %s - unable to determine file type', sourcefile)
126
+ continue
127
+
128
+ date_match = filetype_re.fullmatch(os.path.basename(sourcefile))
129
+ if not date_match:
130
+ logging.debug('Ignoring %s - unable to extract date from filename', sourcefile)
131
+ continue
132
+
133
+ year = date_match.group(2)
134
+ month = date_match.group(3)
135
+ day = date_match.group(4)
136
+
137
+ default_dest_dir = f'{dest_dir}/{year}/{year}-{month}-{day}'
138
+ dest_dir_pattern = f'{default_dest_dir}*'
139
+
140
+ dest_dirs = [path for path in glob.glob(dest_dir_pattern) if os.path.isdir(path)]
141
+
142
+ sourcefile_name = os.path.basename(sourcefile)
143
+
144
+ # Search any matching destination directories to see if the file exists
145
+
146
+ if dest_dirs:
147
+ for dest_dir in dest_dirs:
148
+ if os.path.isfile(os.path.join(dest_dir, sourcefile_name)):
149
+ break
150
+ else:
151
+ dest_dir = sorted(dest_dirs)[0]
152
+ else:
153
+ if not args.dryrun:
154
+ os.makedirs(default_dest_dir)
155
+
156
+ dest_dir = default_dest_dir
157
+
158
+ dest_file = os.path.join(dest_dir, sourcefile_name)
159
+
160
+ if os.path.exists(dest_file):
161
+ logging.debug('Destination file %s already exists', dest_file)
162
+ else:
163
+ logging.info('Copying %s to %s', sourcefile, dest_file)
164
+
165
+ if not args.dryrun:
166
+ shutil.copyfile(sourcefile, dest_file)
167
+
168
+ files_copied += 1
169
+
170
+ print(f'{files_copied} files copied')
171
+
172
+ ################################################################################
173
+
174
+ def localphotosync():
175
+ """Entry point"""
176
+ try:
177
+ args = parse_command_line()
178
+
179
+ media_sync(args)
180
+
181
+ except KeyboardInterrupt:
182
+ sys.exit(1)
183
+
184
+ except BrokenPipeError:
185
+ sys.exit(2)
186
+
187
+ ################################################################################
188
+
189
+ if __name__ == '__main__':
190
+ localphotosync()
@@ -396,7 +396,7 @@ def mg_init(args, config, console):
396
396
 
397
397
  abs_repo_path = absolute_repo_path(args, repo)
398
398
 
399
- config[repo] = { }
399
+ config[repo] = {}
400
400
 
401
401
  default_branch = git.branch(path=abs_repo_path)
402
402
 
@@ -21,21 +21,21 @@ def read_image_hashes(directories):
21
21
  hashes = defaultdict(list)
22
22
 
23
23
  # Walk each directory tree
24
-
24
+
25
25
  for directory in directories:
26
26
  print(f'Scanning directory tree {directory}')
27
-
27
+
28
28
  for root, _, files in os.walk(directory):
29
29
  print(f'Scanning directory {root}')
30
-
30
+
31
31
  for file in files:
32
32
  filepath = os.path.join(root, file)
33
-
33
+
34
34
  fileext = os.path.splitext(file)[1]
35
35
 
36
36
  if fileext.lower() not in ('.jbf', '.ini', '.xml', '.ffs_db'):
37
37
  # Calculate the hash and store path, dimensions and file size under the hash entry in the hashes table
38
-
38
+
39
39
  try:
40
40
  with Image.open(filepath) as image:
41
41
  hash_value = imagehash.average_hash(image, hash_size=12)
@@ -48,9 +48,9 @@ def read_image_hashes(directories):
48
48
 
49
49
  except OSError:
50
50
  sys.stderr.write(f'ERROR: Unable to read {filepath} (size={size})\n')
51
-
51
+
52
52
  # Return the hash table
53
-
53
+
54
54
  return hashes
55
55
 
56
56
  ################################################################################
@@ -62,11 +62,11 @@ def main():
62
62
  parser.add_argument('directories', nargs='*', action='store', help='Directories to search')
63
63
 
64
64
  args = parser.parse_args()
65
-
65
+
66
66
  if not args.directories:
67
67
  print('You must be specify at least one directory')
68
68
  sys.exit(1)
69
-
69
+
70
70
  try:
71
71
  print('Loading cached data')
72
72
 
@@ -78,7 +78,7 @@ def main():
78
78
  hashes = read_image_hashes(args.directories)
79
79
 
80
80
  # Sort the list of hashes so that we can easily find close matches
81
-
81
+
82
82
  print('Sorting hashes')
83
83
 
84
84
  hash_values = sorted([str(hashval) for hashval in hashes])
@@ -70,8 +70,10 @@ def audit(package, version):
70
70
  def main():
71
71
  """ Entry point """
72
72
 
73
- parser = argparse.ArgumentParser(description='Query api.osv.dev to determine whether Python packagers in a requirments.txt file are subject to known security vulnerabilities')
74
- parser.add_argument('requirements', nargs='*', type=str, action='store', help='The requirements file (if not specified, then the script searches for a requirements.txt file)')
73
+ parser = argparse.ArgumentParser(
74
+ description='Query api.osv.dev to determine whether Python packagers in a requirments.txt file are subject to known security vulnerabilities')
75
+ parser.add_argument('requirements', nargs='*', type=str, action='store',
76
+ help='The requirements file (if not specified, then the script searches for a requirements.txt file)')
75
77
  args = parser.parse_args()
76
78
 
77
79
  requirements = args.requirements or glob.glob('**/requirements.txt', recursive=True)
@@ -47,13 +47,13 @@ TF_TAG_ENTRY_IGNORE = re.compile(r'^ +".*" += +".*"')
47
47
  TF_TAG_CHANGE_BLOCK_END = re.compile(r'^ +}$')
48
48
 
49
49
  TF_MISC_REGEX = \
50
- [
51
- { 'regex': re.compile(r'(Read complete after) (\d+s|\d+m\d+s)'), 'replace': r'\1 {ELAPSED}'},
52
- { 'regex': re.compile(r'"(.*:.*)"( = ".*")'), 'replace': r'\1\2'},
53
- { 'regex': re.compile(r'"(.*:.*)"( = \[$)'), 'replace': r'\1\2'},
54
- { 'regex': re.compile(r'^last "terraform apply":$'), 'replace':r'last "terraform apply" which may have affected this plan:'},
55
- { 'find': ' ~ ', 'replace': ' * '},
56
- ]
50
+ [
51
+ {'regex': re.compile(r'(Read complete after) (\d+s|\d+m\d+s)'), 'replace': r'\1 {ELAPSED}'},
52
+ {'regex': re.compile(r'"(.*:.*)"( = ".*")'), 'replace': r'\1\2'},
53
+ {'regex': re.compile(r'"(.*:.*)"( = \[$)'), 'replace': r'\1\2'},
54
+ {'regex': re.compile(r'^last "terraform apply":$'), 'replace': r'last "terraform apply" which may have affected this plan:'},
55
+ {'find': ' ~ ', 'replace': ' * '},
56
+ ]
57
57
 
58
58
  TF_IGNORE_LIST = [
59
59
  {'start': TF_HAS_CHANGED, 'end': TF_HAS_CHANGED_END},
@@ -91,8 +91,10 @@ def parse_command_line():
91
91
  parser.add_argument('--terraform', '-T', action='store_true', help='Clean Terraform plan/apply log files')
92
92
  parser.add_argument('--replace', '-R', action='append', default=None, help='Additional regex replacements in the form "REGEX=REPLACEMENT"')
93
93
  parser.add_argument('--verbose', '-v', action='store_true', help='Output verbose status')
94
- parser.add_argument('--minimal', '-m', action='store_true', help='Remove unnecessary data from the file (e.g. Terraform progress updates (Refreshing..., Reading..., etc.))')
95
- parser.add_argument('--non-minimal', '-M', action='store_true', help='Do not remove unnecessary data from the file (e.g. Terraform progress updates (Refreshing..., Reading..., etc.))')
94
+ parser.add_argument('--minimal', '-m', action='store_true',
95
+ help='Remove unnecessary data from the file (e.g. Terraform progress updates (Refreshing..., Reading..., etc.))')
96
+ parser.add_argument('--non-minimal', '-M', action='store_true',
97
+ help='Do not remove unnecessary data from the file (e.g. Terraform progress updates (Refreshing..., Reading..., etc.))')
96
98
  parser.add_argument('files', nargs='*', default=None, help='The files to convert (use stdin/stout if no input files are specified)')
97
99
 
98
100
  args = parser.parse_args()
@@ -144,7 +146,7 @@ def parse_command_line():
144
146
  for entry in args.replace:
145
147
  regex, replace = entry.split('=')
146
148
  try:
147
- args.regex_replace.append({'regex': re.compile(regex), 'replace':replace})
149
+ args.regex_replace.append({'regex': re.compile(regex), 'replace': replace})
148
150
  except re.error as exc:
149
151
  print(f'ERROR in regular expression {regex}: {exc}')
150
152
  sys.exit(1)
@@ -291,7 +293,7 @@ def cleanfile(args, infile, outfile):
291
293
  # Write normal output, skipping >1 blank lines and skipping ignore blocks when the pre-ignore
292
294
  # count has hit zero.
293
295
 
294
- if clean is not None and not (ignore_until and pre_ignore_count==0):
296
+ if clean is not None and not (ignore_until and pre_ignore_count == 0):
295
297
  if clean != '' or prev_line != '':
296
298
  outfile.write(clean)
297
299
  outfile.write('\n')
@@ -57,7 +57,7 @@ def show_cpu_times(scr, first, w, h, x, y):
57
57
  scr.addstr(y+2, x, 'IRQ:')
58
58
  scr.addstr(y+3, x, 'Soft IRQ:')
59
59
 
60
- x+= w//3
60
+ x += w//3
61
61
 
62
62
  scr.addstr(y+1, x, 'Guest:')
63
63
  scr.addstr(y+2, x, 'Guest Nice:')
@@ -320,7 +320,7 @@ def show_temperatures(scr, first, w, h, x, y):
320
320
 
321
321
  # Panel title and the functions used to update them
322
322
 
323
- BOXES= {
323
+ BOXES = {
324
324
  'System Load': show_system_load,
325
325
  'Disk Access': show_disk_access,
326
326
  'Processes': show_processes,
@@ -186,7 +186,7 @@ def write(txt=None, newline=True, stream=sys.stdout, indent=0, strip=False, clea
186
186
 
187
187
  def error(txt, newline=True, stream=sys.stderr, status=1, prefix=False):
188
188
  """ Write an error message to the specified stream (defaulting to
189
- stderr) and exit with the specified status code (defaulting to 1)
189
+ stderr) and exit with the specified status code (defaulting to 1)
190
190
  Prefix the output with 'ERROR:' in red if prefix==True """
191
191
 
192
192
  if prefix:
@@ -20,6 +20,7 @@ import thingy.dc_util as dc_util
20
20
  __all__ = ['Dircolors']
21
21
 
22
22
  _CODE_MAP = OrderedDict()
23
+
23
24
  def _init_code_map():
24
25
  """ mapping between the key name in the .dircolors file and the two letter
25
26
  code found in the LS_COLORS environment variable.
@@ -53,6 +54,7 @@ class Dircolors:
53
54
  """ Main dircolors class. Contains a database of formats corresponding to file types,
54
55
  modes, and extensions. Use the format() method to check a file and color it appropriately.
55
56
  """
57
+
56
58
  def __init__(self, load=True):
57
59
  """ Initialize a Dircolors object. If load=True (the default), then try
58
60
  to load dircolors info from the LS_COLORS environment variable.
@@ -98,7 +100,7 @@ class Dircolors:
98
100
  try:
99
101
  code, color = item.split('=', 1)
100
102
  except ValueError:
101
- continue # no key=value, just ignore
103
+ continue # no key=value, just ignore
102
104
  if code.startswith('*.'):
103
105
  self._extensions[code[1:]] = color
104
106
  else:
@@ -134,7 +136,7 @@ class Dircolors:
134
136
  elif isinstance(database, TextIOBase):
135
137
  file = database
136
138
  else:
137
- raise ValueError('database must be str or io.TextIOBase, not %s'%type(database))
139
+ raise ValueError('database must be str or io.TextIOBase, not %s' % type(database))
138
140
 
139
141
  try:
140
142
  for line in file:
@@ -147,18 +149,18 @@ class Dircolors:
147
149
  split = line.split()
148
150
  if len(split) != 2:
149
151
  if strict:
150
- raise ValueError('Warning: unable to parse dircolors line "%s"'%line)
152
+ raise ValueError('Warning: unable to parse dircolors line "%s"' % line)
151
153
  continue
152
154
 
153
155
  key, val = split
154
156
  if key == 'TERM':
155
- continue # ignore TERM directives
157
+ continue # ignore TERM directives
156
158
  elif key in _CODE_MAP:
157
159
  self._codes[_CODE_MAP[key]] = val
158
160
  elif key.startswith('.'):
159
161
  self._extensions[key] = val
160
162
  elif strict:
161
- raise ValueError('Warning: unable to parse dircolors line "%s"'%line)
163
+ raise ValueError('Warning: unable to parse dircolors line "%s"' % line)
162
164
  # elif not strict, skip
163
165
 
164
166
  if self._codes or self._extensions:
@@ -184,14 +186,14 @@ class Dircolors:
184
186
  # change .xyz to *.xyz
185
187
  yield '*' + pair[0], pair[1]
186
188
 
187
- return ':'.join('%s=%s'%pair for pair in gen_pairs())
189
+ return ':'.join('%s=%s' % pair for pair in gen_pairs())
188
190
 
189
191
  def _format_code(self, text, code):
190
192
  """ format text with an lscolors code. Return text unmodified if code
191
193
  isn't found in the database """
192
194
  val = self._codes.get(code, None)
193
195
  if val:
194
- return '\033[%sm%s\033[%sm'%(val, text, self._codes.get('rs', '0'))
196
+ return '\033[%sm%s\033[%sm' % (val, text, self._codes.get('rs', '0'))
195
197
  return text
196
198
 
197
199
  def _format_ext(self, text, ext):
@@ -200,7 +202,7 @@ class Dircolors:
200
202
  text need not actually end in '.ext' """
201
203
  val = self._extensions.get(ext, '0')
202
204
  if val:
203
- return '\033[%sm%s\033[%sm'%(val, text, self._codes.get('rs', '0'))
205
+ return '\033[%sm%s\033[%sm' % (val, text, self._codes.get('rs', '0'))
204
206
  return text
205
207
 
206
208
  def format_mode(self, text, mode):
@@ -225,7 +227,7 @@ class Dircolors:
225
227
  elif isinstance(mode, os.stat_result):
226
228
  mode = mode.st_mode
227
229
  else:
228
- raise ValueError('mode must be int or os.stat_result, not %s'%type(mode))
230
+ raise ValueError('mode must be int or os.stat_result, not %s' % type(mode))
229
231
 
230
232
  if mode:
231
233
  if stat.S_ISDIR(mode):
@@ -244,13 +246,13 @@ class Dircolors:
244
246
  # special file?
245
247
  # pylint: disable=bad-whitespace
246
248
  special_types = (
247
- (stat.S_IFLNK, 'ln'), # symlink
248
- (stat.S_IFIFO, 'pi'), # pipe (FIFO)
249
- (stat.S_IFSOCK, 'so'), # socket
250
- (stat.S_IFBLK, 'bd'), # block device
251
- (stat.S_IFCHR, 'cd'), # character device
252
- (stat.S_ISUID, 'su'), # setuid
253
- (stat.S_ISGID, 'sg'), # setgid
249
+ (stat.S_IFLNK, 'ln'), # symlink
250
+ (stat.S_IFIFO, 'pi'), # pipe (FIFO)
251
+ (stat.S_IFSOCK, 'so'), # socket
252
+ (stat.S_IFBLK, 'bd'), # block device
253
+ (stat.S_IFCHR, 'cd'), # character device
254
+ (stat.S_ISUID, 'su'), # setuid
255
+ (stat.S_ISGID, 'sg'), # setgid
254
256
  )
255
257
 
256
258
  for mask, code in special_types:
@@ -290,13 +292,13 @@ class Dircolors:
290
292
  try:
291
293
  statbuf = dc_util.stat_at(file, cwd, follow_symlinks)
292
294
  except OSError as e:
293
- return '%s [Error stat-ing: %s]'%(file, e.strerror)
295
+ return '%s [Error stat-ing: %s]' % (file, e.strerror)
294
296
 
295
297
  mode = statbuf.st_mode
296
298
  if (not follow_symlinks) and show_target and stat.S_ISLNK(mode):
297
299
  target_path = dc_util.readlink_at(file, cwd)
298
300
  try:
299
- dc_util.stat_at(target_path, cwd) # check for broken link
301
+ dc_util.stat_at(target_path, cwd) # check for broken link
300
302
  target = self.format(target_path, cwd, False, False)
301
303
  except OSError:
302
304
  # format as "orphan"
@@ -1287,7 +1287,6 @@ def log(branch1, branch2=None):
1287
1287
 
1288
1288
  def clean(recurse=False, force=False, dry_run=False, quiet=False,
1289
1289
  exclude=None, ignore_rules=False, remove_only_ignored=False, path=None):
1290
-
1291
1290
  """ Run git clean """
1292
1291
 
1293
1292
  cmd = ['clean']
@@ -82,6 +82,6 @@ class PopUp():
82
82
  time.sleep(1 - elapsed)
83
83
 
84
84
  del self.panel
85
-
85
+
86
86
  if self.refresh:
87
87
  self.screen.refresh()
@@ -148,7 +148,7 @@ class Pane():
148
148
 
149
149
  filestat = os.stat(filename, follow_symlinks=False)
150
150
 
151
- info = {'name':filename,
151
+ info = {'name': filename,
152
152
  'mode': filestat.st_mode,
153
153
  'uid': filestat.st_uid,
154
154
  'gid': filestat.st_gid,
@@ -310,7 +310,7 @@ class Pane():
310
310
  else:
311
311
  self.screen.clrtoeol()
312
312
 
313
- #if len(filename) < self.width:
313
+ # if len(filename) < self.width:
314
314
  # self.screen.addstr(self.file_list_y + ypos, len(filename), ' ' * (self.width - len(filename)), normal_colour)
315
315
 
316
316
  current_dir = path.trimpath(self.current_dir, self.width)
@@ -541,7 +541,7 @@ class Pane():
541
541
 
542
542
  self.height = height
543
543
  self.file_list_h = height-1
544
- self.width = pane_width-1 # TODO: Why '-1'?
544
+ self.width = pane_width-1 # TODO: Why '-1'?
545
545
  self.screen.resize(height, pane_width)
546
546
  self.screen.mvwin(y, x + pane_width*self.index)
547
547