From 46f506cb9086e89070ae7cfc28ad79fb4e92e766 Mon Sep 17 00:00:00 2001 From: mandarons Date: Sat, 7 Oct 2023 16:10:03 +0000 Subject: [PATCH] deploy: 77eceb3e9a8d42eb8615cc6a4b25ec8ebe9275b2 --- test-coverage/index.html | 14 +- test-coverage/src___init___py.html | 2 +- test-coverage/src_config_parser_py.html | 16 +- test-coverage/src_email_message_py.html | 2 +- test-coverage/src_notify_py.html | 2 +- test-coverage/src_sync_drive_py.html | 2 +- test-coverage/src_sync_photos_py.html | 433 +++++++++++++----------- test-coverage/src_sync_py.html | 2 +- test-coverage/src_usage_py.html | 2 +- test-coverage/status.json | 2 +- 10 files changed, 254 insertions(+), 223 deletions(-) diff --git a/test-coverage/index.html b/test-coverage/index.html index d45fe97bf..22d807d93 100644 --- a/test-coverage/index.html +++ b/test-coverage/index.html @@ -53,10 +53,10 @@

Coverage report: Total - 795 + 807 0 0 - 100% + 100% @@ -69,10 +69,10 @@

Coverage report: src/config_parser.py - 186 + 193 0 0 - 100% + 100% src/email_message.py @@ -104,10 +104,10 @@

Coverage report: src/sync_photos.py - 103 + 108 0 0 - 100% + 100% src/usage.py @@ -126,7 +126,7 @@

Coverage report:

coverage.py v5.4, - created at 2023-10-05 15:53 +0000 + created at 2023-10-07 16:08 +0000

diff --git a/test-coverage/src___init___py.html b/test-coverage/src___init___py.html index 482655730..9dbcb958a 100644 --- a/test-coverage/src___init___py.html +++ b/test-coverage/src___init___py.html @@ -194,7 +194,7 @@

« index     coverage.py v5.4, - created at 2023-10-05 15:53 +0000 + created at 2023-10-07 16:08 +0000

diff --git a/test-coverage/src_config_parser_py.html b/test-coverage/src_config_parser_py.html index 7f2a5223c..b68e77373 100644 --- a/test-coverage/src_config_parser_py.html +++ b/test-coverage/src_config_parser_py.html @@ -22,8 +22,8 @@

Coverage for src/config_parser.py :

Show keyboard shortcuts

- 186 statements   - + 193 statements   +

@@ -409,12 +409,22 @@

355 region = "global" 

356 

357 return region 

+

358 

+

359 

+

360def get_photos_folder_format(config): 

+

361 """Return filename format or None.""" 

+

362 fmt = None 

+

363 config_path = ["photos", "folder_format"] 

+

364 if traverse_config_path(config=config, config_path=config_path): 

+

365 fmt = get_config_value(config=config, config_path=config_path) 

+

366 LOGGER.info(f"Using format {fmt}.") 

+

367 return fmt 

diff --git a/test-coverage/src_email_message_py.html b/test-coverage/src_email_message_py.html index 69fafaa05..754f7a7b5 100644 --- a/test-coverage/src_email_message_py.html +++ b/test-coverage/src_email_message_py.html @@ -108,7 +108,7 @@

« index     coverage.py v5.4, - created at 2023-10-05 15:53 +0000 + created at 2023-10-07 16:08 +0000

diff --git a/test-coverage/src_notify_py.html b/test-coverage/src_notify_py.html index 7bfbca7ee..bfb7fc148 100644 --- a/test-coverage/src_notify_py.html +++ b/test-coverage/src_notify_py.html @@ -119,7 +119,7 @@

« index     coverage.py v5.4, - created at 2023-10-05 15:53 +0000 + created at 2023-10-07 16:08 +0000

diff --git a/test-coverage/src_sync_drive_py.html b/test-coverage/src_sync_drive_py.html index 52f84f41c..aa715e805 100644 --- a/test-coverage/src_sync_drive_py.html +++ b/test-coverage/src_sync_drive_py.html @@ -389,7 +389,7 @@

« index     coverage.py v5.4, - created at 2023-10-05 15:53 +0000 + created at 2023-10-07 16:08 +0000

diff --git a/test-coverage/src_sync_photos_py.html b/test-coverage/src_sync_photos_py.html index b717abcbd..48f6ef8a6 100644 --- a/test-coverage/src_sync_photos_py.html +++ b/test-coverage/src_sync_photos_py.html @@ -22,8 +22,8 @@

Coverage for src/sync_photos.py :

Show keyboard shortcuts

- 103 statements   - + 108 statements   +

@@ -76,7 +76,7 @@

22 return False 

23 

24 

-

25def generate_file_name(photo, file_size, destination_path): 

+

25def generate_file_name(photo, file_size, destination_path, folder_format): 

26 """Generate full path to file.""" 

27 filename = photo.filename 

28 name, extension = filename.rsplit(".", 1) if "." in filename else [filename, ""] 

@@ -94,218 +94,239 @@

40 else f'{"__".join([name, file_size, base64.urlsafe_b64encode(photo.id.encode()).decode()])}.{extension}', 

41 ) 

42 

-

43 file_size_id_path_norm = unicodedata.normalize("NFC", file_size_id_path) 

-

44 

-

45 if os.path.isfile(file_path): 

-

46 os.rename(file_path, file_size_id_path) 

-

47 if os.path.isfile(file_size_path): 

-

48 os.rename(file_size_path, file_size_id_path) 

-

49 if os.path.isfile(file_size_id_path): 

-

50 os.rename(file_size_id_path, file_size_id_path_norm) 

-

51 return file_size_id_path_norm 

-

52 

+

43 if folder_format is not None: 

+

44 folder = photo.created.strftime(folder_format) 

+

45 file_size_id_path = os.path.join( 

+

46 destination_path, 

+

47 folder, 

+

48 f'{"__".join([name, file_size, base64.urlsafe_b64encode(photo.id.encode()).decode()])}' 

+

49 if extension == "" 

+

50 else f'{"__".join([name, file_size, base64.urlsafe_b64encode(photo.id.encode()).decode()])}.{extension}', 

+

51 ) 

+

52 os.makedirs(os.path.join(destination_path, folder), exist_ok=True) 

53 

-

54def photo_exists(photo, file_size, local_path): 

-

55 """Check if photo exist locally.""" 

-

56 if photo and local_path and os.path.isfile(local_path): 

-

57 local_size = os.path.getsize(local_path) 

-

58 remote_size = int(photo.versions[file_size]["size"]) 

-

59 if local_size == remote_size: 

-

60 LOGGER.debug(f"No changes detected. Skipping the file {local_path} ...") 

-

61 return True 

-

62 else: 

-

63 LOGGER.debug( 

-

64 f"Change detected: local_file_size is {local_size} and remote_file_size is {remote_size}." 

-

65 ) 

-

66 return False 

-

67 

-

68 

-

69def download_photo(photo, file_size, destination_path): 

-

70 """Download photo from server.""" 

-

71 if not (photo and file_size and destination_path): 

-

72 return False 

-

73 LOGGER.info(f"Downloading {destination_path} ...") 

-

74 try: 

-

75 download = photo.download(file_size) 

-

76 with open(destination_path, "wb") as file_out: 

-

77 shutil.copyfileobj(download.raw, file_out) 

-

78 local_modified_time = time.mktime(photo.added_date.timetuple()) 

-

79 os.utime(destination_path, (local_modified_time, local_modified_time)) 

-

80 except (exceptions.ICloudPyAPIResponseException, FileNotFoundError, Exception) as e: 

-

81 LOGGER.error(f"Failed to download {destination_path}: {str(e)}") 

-

82 return False 

-

83 return True 

-

84 

-

85 

-

86def process_photo(photo, file_size, destination_path, files): 

-

87 """Process photo details.""" 

-

88 photo_path = generate_file_name( 

-

89 photo=photo, file_size=file_size, destination_path=destination_path 

-

90 ) 

-

91 if file_size not in photo.versions: 

-

92 LOGGER.warning( 

-

93 f"File size {file_size} not found on server. Skipping the photo {photo_path} ..." 

-

94 ) 

-

95 return False 

-

96 if files is not None: 

-

97 files.add(photo_path) 

-

98 if photo_exists(photo, file_size, photo_path): 

-

99 return False 

-

100 download_photo(photo, file_size, photo_path) 

-

101 return True 

-

102 

-

103 

-

104def sync_album(album, destination_path, file_sizes, extensions=None, files=None): 

-

105 """Sync given album.""" 

-

106 if album is None or destination_path is None or file_sizes is None: 

-

107 return None 

-

108 os.makedirs(unicodedata.normalize("NFC", destination_path), exist_ok=True) 

-

109 LOGGER.info(f"Syncing {album.title}") 

-

110 for photo in album: 

-

111 if photo_wanted(photo, extensions): 

-

112 for file_size in file_sizes: 

-

113 process_photo(photo, file_size, destination_path, files) 

-

114 else: 

-

115 LOGGER.debug(f"Skipping the unwanted photo {photo.filename}.") 

-

116 for subalbum in album.subalbums: 

-

117 sync_album( 

-

118 album.subalbums[subalbum], 

-

119 os.path.join(destination_path, subalbum), 

-

120 file_sizes, 

-

121 extensions, 

-

122 files, 

-

123 ) 

-

124 return True 

-

125 

-

126 

-

127def remove_obsolete(destination_path, files): 

-

128 """Remove local obsolete file.""" 

-

129 removed_paths = set() 

-

130 if not (destination_path and files is not None): 

-

131 return removed_paths 

-

132 for path in Path(destination_path).rglob("*"): 

-

133 local_file = str(path.absolute()) 

-

134 if local_file not in files: 

-

135 if path.is_file(): 

-

136 LOGGER.info(f"Removing {local_file} ...") 

-

137 path.unlink(missing_ok=True) 

-

138 removed_paths.add(local_file) 

-

139 return removed_paths 

-

140 

-

141 

-

142def sync_photos(config, photos): 

-

143 """Sync all photos.""" 

-

144 destination_path = config_parser.prepare_photos_destination(config=config) 

-

145 filters = config_parser.get_photos_filters(config=config) 

-

146 files = set() 

-

147 download_all = config_parser.get_photos_all_albums(config=config) 

-

148 if download_all: 

-

149 for album in photos.albums.keys(): 

-

150 sync_album( 

-

151 album=photos.albums[album], 

-

152 destination_path=os.path.join(destination_path, album), 

-

153 file_sizes=filters["file_sizes"], 

-

154 extensions=filters["extensions"], 

-

155 files=files, 

-

156 ) 

-

157 elif filters["albums"]: 

-

158 for album in iter(filters["albums"]): 

-

159 sync_album( 

-

160 album=photos.albums[album], 

-

161 destination_path=os.path.join(destination_path, album), 

-

162 file_sizes=filters["file_sizes"], 

-

163 extensions=filters["extensions"], 

-

164 files=files, 

-

165 ) 

-

166 else: 

-

167 sync_album( 

-

168 album=photos.all, 

-

169 destination_path=os.path.join(destination_path, "all"), 

-

170 file_sizes=filters["file_sizes"], 

-

171 extensions=filters["extensions"], 

-

172 files=files, 

-

173 ) 

-

174 

-

175 if config_parser.get_photos_remove_obsolete(config=config): 

-

176 remove_obsolete(destination_path, files) 

-

177 

-

178 

-

179# def enable_debug(): 

-

180# import contextlib 

-

181# import http.client 

-

182# import logging 

-

183# import requests 

-

184# import warnings 

-

185 

-

186# # from pprint import pprint 

-

187# # from icloudpy import ICloudPyService 

-

188# from urllib3.exceptions import InsecureRequestWarning 

-

189 

-

190# # Handle certificate warnings by ignoring them 

-

191# old_merge_environment_settings = requests.Session.merge_environment_settings 

-

192 

-

193# @contextlib.contextmanager 

-

194# def no_ssl_verification(): 

-

195# opened_adapters = set() 

-

196 

-

197# def merge_environment_settings(self, url, proxies, stream, verify, cert): 

-

198# # Verification happens only once per connection so we need to close 

-

199# # all the opened adapters once we're done. Otherwise, the effects of 

-

200# # verify=False persist beyond the end of this context manager. 

-

201# opened_adapters.add(self.get_adapter(url)) 

-

202 

-

203# settings = old_merge_environment_settings( 

-

204# self, url, proxies, stream, verify, cert 

-

205# ) 

-

206# settings["verify"] = False 

-

207 

-

208# return settings 

-

209 

-

210# requests.Session.merge_environment_settings = merge_environment_settings 

-

211 

-

212# try: 

-

213# with warnings.catch_warnings(): 

-

214# warnings.simplefilter("ignore", InsecureRequestWarning) 

-

215# yield 

-

216# finally: 

-

217# requests.Session.merge_environment_settings = old_merge_environment_settings 

-

218 

-

219# for adapter in opened_adapters: 

-

220# try: 

-

221# adapter.close() 

-

222# except Exception as e: 

-

223# pass 

-

224 

-

225# # Monkeypatch the http client for full debugging output 

-

226# httpclient_logger = logging.getLogger("http.client") 

-

227 

-

228# def httpclient_logging_patch(level=logging.DEBUG): 

-

229# """Enable HTTPConnection debug logging to the logging framework""" 

+

54 file_size_id_path_norm = unicodedata.normalize("NFC", file_size_id_path) 

+

55 

+

56 if os.path.isfile(file_path): 

+

57 os.rename(file_path, file_size_id_path) 

+

58 if os.path.isfile(file_size_path): 

+

59 os.rename(file_size_path, file_size_id_path) 

+

60 if os.path.isfile(file_size_id_path): 

+

61 os.rename(file_size_id_path, file_size_id_path_norm) 

+

62 return file_size_id_path_norm 

+

63 

+

64 

+

65def photo_exists(photo, file_size, local_path): 

+

66 """Check if photo exist locally.""" 

+

67 if photo and local_path and os.path.isfile(local_path): 

+

68 local_size = os.path.getsize(local_path) 

+

69 remote_size = int(photo.versions[file_size]["size"]) 

+

70 if local_size == remote_size: 

+

71 LOGGER.debug(f"No changes detected. Skipping the file {local_path} ...") 

+

72 return True 

+

73 else: 

+

74 LOGGER.debug( 

+

75 f"Change detected: local_file_size is {local_size} and remote_file_size is {remote_size}." 

+

76 ) 

+

77 return False 

+

78 

+

79 

+

80def download_photo(photo, file_size, destination_path): 

+

81 """Download photo from server.""" 

+

82 if not (photo and file_size and destination_path): 

+

83 return False 

+

84 LOGGER.info(f"Downloading {destination_path} ...") 

+

85 try: 

+

86 download = photo.download(file_size) 

+

87 with open(destination_path, "wb") as file_out: 

+

88 shutil.copyfileobj(download.raw, file_out) 

+

89 local_modified_time = time.mktime(photo.added_date.timetuple()) 

+

90 os.utime(destination_path, (local_modified_time, local_modified_time)) 

+

91 except (exceptions.ICloudPyAPIResponseException, FileNotFoundError, Exception) as e: 

+

92 LOGGER.error(f"Failed to download {destination_path}: {str(e)}") 

+

93 return False 

+

94 return True 

+

95 

+

96 

+

97def process_photo(photo, file_size, destination_path, files, folder_format): 

+

98 """Process photo details.""" 

+

99 photo_path = generate_file_name( 

+

100 photo=photo, 

+

101 file_size=file_size, 

+

102 destination_path=destination_path, 

+

103 folder_format=folder_format, 

+

104 ) 

+

105 if file_size not in photo.versions: 

+

106 LOGGER.warning( 

+

107 f"File size {file_size} not found on server. Skipping the photo {photo_path} ..." 

+

108 ) 

+

109 return False 

+

110 if files is not None: 

+

111 files.add(photo_path) 

+

112 if photo_exists(photo, file_size, photo_path): 

+

113 return False 

+

114 download_photo(photo, file_size, photo_path) 

+

115 return True 

+

116 

+

117 

+

118def sync_album( 

+

119 album, destination_path, file_sizes, extensions=None, files=None, folder_format=None 

+

120): 

+

121 """Sync given album.""" 

+

122 if album is None or destination_path is None or file_sizes is None: 

+

123 return None 

+

124 os.makedirs(unicodedata.normalize("NFC", destination_path), exist_ok=True) 

+

125 LOGGER.info(f"Syncing {album.title}") 

+

126 for photo in album: 

+

127 if photo_wanted(photo, extensions): 

+

128 for file_size in file_sizes: 

+

129 process_photo(photo, file_size, destination_path, files, folder_format) 

+

130 else: 

+

131 LOGGER.debug(f"Skipping the unwanted photo {photo.filename}.") 

+

132 for subalbum in album.subalbums: 

+

133 sync_album( 

+

134 album.subalbums[subalbum], 

+

135 os.path.join(destination_path, subalbum), 

+

136 file_sizes, 

+

137 extensions, 

+

138 files, 

+

139 folder_format, 

+

140 ) 

+

141 return True 

+

142 

+

143 

+

144def remove_obsolete(destination_path, files): 

+

145 """Remove local obsolete file.""" 

+

146 removed_paths = set() 

+

147 if not (destination_path and files is not None): 

+

148 return removed_paths 

+

149 for path in Path(destination_path).rglob("*"): 

+

150 local_file = str(path.absolute()) 

+

151 if local_file not in files: 

+

152 if path.is_file(): 

+

153 LOGGER.info(f"Removing {local_file} ...") 

+

154 path.unlink(missing_ok=True) 

+

155 removed_paths.add(local_file) 

+

156 return removed_paths 

+

157 

+

158 

+

159def sync_photos(config, photos): 

+

160 """Sync all photos.""" 

+

161 destination_path = config_parser.prepare_photos_destination(config=config) 

+

162 filters = config_parser.get_photos_filters(config=config) 

+

163 files = set() 

+

164 download_all = config_parser.get_photos_all_albums(config=config) 

+

165 folder_format = config_parser.get_photos_folder_format(config=config) 

+

166 if download_all: 

+

167 for album in photos.albums.keys(): 

+

168 sync_album( 

+

169 album=photos.albums[album], 

+

170 destination_path=os.path.join(destination_path, album), 

+

171 file_sizes=filters["file_sizes"], 

+

172 extensions=filters["extensions"], 

+

173 files=files, 

+

174 folder_format=folder_format, 

+

175 ) 

+

176 elif filters["albums"]: 

+

177 for album in iter(filters["albums"]): 

+

178 sync_album( 

+

179 album=photos.albums[album], 

+

180 destination_path=os.path.join(destination_path, album), 

+

181 file_sizes=filters["file_sizes"], 

+

182 extensions=filters["extensions"], 

+

183 files=files, 

+

184 folder_format=folder_format, 

+

185 ) 

+

186 else: 

+

187 sync_album( 

+

188 album=photos.all, 

+

189 destination_path=os.path.join(destination_path, "all"), 

+

190 file_sizes=filters["file_sizes"], 

+

191 extensions=filters["extensions"], 

+

192 files=files, 

+

193 folder_format=folder_format, 

+

194 ) 

+

195 

+

196 if config_parser.get_photos_remove_obsolete(config=config): 

+

197 remove_obsolete(destination_path, files) 

+

198 

+

199 

+

200# def enable_debug(): 

+

201# import contextlib 

+

202# import http.client 

+

203# import logging 

+

204# import requests 

+

205# import warnings 

+

206 

+

207# # from pprint import pprint 

+

208# # from icloudpy import ICloudPyService 

+

209# from urllib3.exceptions import InsecureRequestWarning 

+

210 

+

211# # Handle certificate warnings by ignoring them 

+

212# old_merge_environment_settings = requests.Session.merge_environment_settings 

+

213 

+

214# @contextlib.contextmanager 

+

215# def no_ssl_verification(): 

+

216# opened_adapters = set() 

+

217 

+

218# def merge_environment_settings(self, url, proxies, stream, verify, cert): 

+

219# # Verification happens only once per connection so we need to close 

+

220# # all the opened adapters once we're done. Otherwise, the effects of 

+

221# # verify=False persist beyond the end of this context manager. 

+

222# opened_adapters.add(self.get_adapter(url)) 

+

223 

+

224# settings = old_merge_environment_settings( 

+

225# self, url, proxies, stream, verify, cert 

+

226# ) 

+

227# settings["verify"] = False 

+

228 

+

229# return settings 

230 

-

231# def httpclient_log(*args): 

-

232# httpclient_logger.log(level, " ".join(args)) 

-

233 

-

234# # mask the print() built-in in the http.client module to use 

-

235# # logging instead 

-

236# http.client.print = httpclient_log 

-

237# # enable debugging 

-

238# http.client.HTTPConnection.debuglevel = 1 

+

231# requests.Session.merge_environment_settings = merge_environment_settings 

+

232 

+

233# try: 

+

234# with warnings.catch_warnings(): 

+

235# warnings.simplefilter("ignore", InsecureRequestWarning) 

+

236# yield 

+

237# finally: 

+

238# requests.Session.merge_environment_settings = old_merge_environment_settings 

239 

-

240# # Enable general debug logging 

-

241# logging.basicConfig(filename="log1.txt", encoding="utf-8", level=logging.DEBUG) 

-

242 

-

243# httpclient_logging_patch() 

-

244 

+

240# for adapter in opened_adapters: 

+

241# try: 

+

242# adapter.close() 

+

243# except Exception as e: 

+

244# pass 

245 

-

246# if __name__ == "__main__": 

-

247# # enable_debug() 

-

248# sync_photos() 

+

246# # Monkeypatch the http client for full debugging output 

+

247# httpclient_logger = logging.getLogger("http.client") 

+

248 

+

249# def httpclient_logging_patch(level=logging.DEBUG): 

+

250# """Enable HTTPConnection debug logging to the logging framework""" 

+

251 

+

252# def httpclient_log(*args): 

+

253# httpclient_logger.log(level, " ".join(args)) 

+

254 

+

255# # mask the print() built-in in the http.client module to use 

+

256# # logging instead 

+

257# http.client.print = httpclient_log 

+

258# # enable debugging 

+

259# http.client.HTTPConnection.debuglevel = 1 

+

260 

+

261# # Enable general debug logging 

+

262# logging.basicConfig(filename="log1.txt", encoding="utf-8", level=logging.DEBUG) 

+

263 

+

264# httpclient_logging_patch() 

+

265 

+

266 

+

267# if __name__ == "__main__": 

+

268# # enable_debug() 

+

269# sync_photos() 

diff --git a/test-coverage/src_sync_py.html b/test-coverage/src_sync_py.html index d68c19f73..bd46f2ea0 100644 --- a/test-coverage/src_sync_py.html +++ b/test-coverage/src_sync_py.html @@ -205,7 +205,7 @@

« index     coverage.py v5.4, - created at 2023-10-05 15:53 +0000 + created at 2023-10-07 16:08 +0000

diff --git a/test-coverage/src_usage_py.html b/test-coverage/src_usage_py.html index c44683dca..365c1180b 100644 --- a/test-coverage/src_usage_py.html +++ b/test-coverage/src_usage_py.html @@ -189,7 +189,7 @@

« index     coverage.py v5.4, - created at 2023-10-05 15:53 +0000 + created at 2023-10-07 16:08 +0000

diff --git a/test-coverage/status.json b/test-coverage/status.json index f2de0a46e..990792f4a 100644 --- a/test-coverage/status.json +++ b/test-coverage/status.json @@ -1 +1 @@ -{"format":2,"version":"5.4","globals":"39d500336f42ed1e3d49ba0d1ecd0eea","files":{"src___init___py":{"hash":"620001c02d5bdfe7860d7b8016f7e65c","index":{"nums":[1,76,0,0,0,0,0],"html_filename":"src___init___py.html","relative_filename":"src/__init__.py"}},"src_config_parser_py":{"hash":"5e7b6c990389fa4dbab436ef074dfac7","index":{"nums":[1,186,0,0,0,0,0],"html_filename":"src_config_parser_py.html","relative_filename":"src/config_parser.py"}},"src_email_message_py":{"hash":"f1cd58e66273dace4abeac30f6057ce9","index":{"nums":[1,33,0,0,0,0,0],"html_filename":"src_email_message_py.html","relative_filename":"src/email_message.py"}},"src_notify_py":{"hash":"4f082735b045daa2964183884e816c3e","index":{"nums":[1,44,0,0,0,0,0],"html_filename":"src_notify_py.html","relative_filename":"src/notify.py"}},"src_sync_py":{"hash":"f2fbc097ddece783aad5358cc8452069","index":{"nums":[1,78,0,0,0,0,0],"html_filename":"src_sync_py.html","relative_filename":"src/sync.py"}},"src_sync_drive_py":{"hash":"1c39ab5825fbf021fdbb87f462f56f7f","index":{"nums":[1,189,0,0,0,0,0],"html_filename":"src_sync_drive_py.html","relative_filename":"src/sync_drive.py"}},"src_sync_photos_py":{"hash":"431a95e13820446163e4f504d363e341","index":{"nums":[1,103,0,0,0,0,0],"html_filename":"src_sync_photos_py.html","relative_filename":"src/sync_photos.py"}},"src_usage_py":{"hash":"a9d2ca932b8d5f40998bd6aaf25b232d","index":{"nums":[1,86,0,0,0,0,0],"html_filename":"src_usage_py.html","relative_filename":"src/usage.py"}}}} \ No newline at end of file +{"format":2,"version":"5.4","globals":"39d500336f42ed1e3d49ba0d1ecd0eea","files":{"src___init___py":{"hash":"620001c02d5bdfe7860d7b8016f7e65c","index":{"nums":[1,76,0,0,0,0,0],"html_filename":"src___init___py.html","relative_filename":"src/__init__.py"}},"src_config_parser_py":{"hash":"ee5905e37dfd478ec047e08a782c4512","index":{"nums":[1,193,0,0,0,0,0],"html_filename":"src_config_parser_py.html","relative_filename":"src/config_parser.py"}},"src_email_message_py":{"hash":"f1cd58e66273dace4abeac30f6057ce9","index":{"nums":[1,33,0,0,0,0,0],"html_filename":"src_email_message_py.html","relative_filename":"src/email_message.py"}},"src_notify_py":{"hash":"4f082735b045daa2964183884e816c3e","index":{"nums":[1,44,0,0,0,0,0],"html_filename":"src_notify_py.html","relative_filename":"src/notify.py"}},"src_sync_py":{"hash":"f2fbc097ddece783aad5358cc8452069","index":{"nums":[1,78,0,0,0,0,0],"html_filename":"src_sync_py.html","relative_filename":"src/sync.py"}},"src_sync_drive_py":{"hash":"1c39ab5825fbf021fdbb87f462f56f7f","index":{"nums":[1,189,0,0,0,0,0],"html_filename":"src_sync_drive_py.html","relative_filename":"src/sync_drive.py"}},"src_sync_photos_py":{"hash":"8c5036fc24c61fbb2cae8b4b81c7c663","index":{"nums":[1,108,0,0,0,0,0],"html_filename":"src_sync_photos_py.html","relative_filename":"src/sync_photos.py"}},"src_usage_py":{"hash":"a9d2ca932b8d5f40998bd6aaf25b232d","index":{"nums":[1,86,0,0,0,0,0],"html_filename":"src_usage_py.html","relative_filename":"src/usage.py"}}}} \ No newline at end of file