webchanges 3.22__tar.gz → 3.23.0__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.
- {webchanges-3.22/webchanges.egg-info → webchanges-3.23.0}/PKG-INFO +1 -1
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/__init__.py +1 -1
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/command.py +4 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/differs.py +205 -20
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/mailer.py +45 -3
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/reporters.py +1 -1
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/util.py +1 -1
- {webchanges-3.22 → webchanges-3.23.0/webchanges.egg-info}/PKG-INFO +1 -1
- {webchanges-3.22 → webchanges-3.23.0}/LICENSE +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/MANIFEST.in +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/README.rst +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/pyproject.toml +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/requirements.txt +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/setup.cfg +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/_vendored/__init__.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/_vendored/case_insensitive_dict.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/_vendored/packaging_version.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/cli.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/config.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/filters.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/handler.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/jobs.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/main.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/py.typed +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/storage.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/storage_minidb.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges/worker.py +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges.egg-info/SOURCES.txt +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges.egg-info/dependency_links.txt +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges.egg-info/entry_points.txt +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges.egg-info/requires.txt +0 -0
- {webchanges-3.22 → webchanges-3.23.0}/webchanges.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: webchanges
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.23.0
|
|
4
4
|
Summary: Check web (or command output) for changes since last run and notify. Anonymously alerts you of web changes, with
|
|
5
5
|
Author-email: Mike Borsetti <mike+webchanges@borsetti.com>
|
|
6
6
|
Maintainer-email: Mike Borsetti <mike+webchanges@borsetti.com>
|
|
@@ -22,7 +22,7 @@ __project_name__ = __package__
|
|
|
22
22
|
# * MINOR version when you add functionality in a backwards compatible manner, and
|
|
23
23
|
# * MICRO or PATCH version when you make backwards compatible bug fixes. We no longer use '0'
|
|
24
24
|
# If unsure on increments, use pkg_resources.parse_version to parse
|
|
25
|
-
__version__ = '3.
|
|
25
|
+
__version__ = '3.23.0'
|
|
26
26
|
__description__ = (
|
|
27
27
|
'Check web (or command output) for changes since last run and notify.\n'
|
|
28
28
|
'\n'
|
|
@@ -514,10 +514,12 @@ class UrlwatchCommand:
|
|
|
514
514
|
job_state.new_data = history_data[i].data
|
|
515
515
|
job_state.new_timestamp = history_data[i].timestamp
|
|
516
516
|
job_state.new_etag = history_data[i].etag
|
|
517
|
+
job_state.new_mime_type = history_data[i].mime_type
|
|
517
518
|
if not job.compared_versions or job.compared_versions == 1:
|
|
518
519
|
job_state.old_data = history_data[i + 1].data
|
|
519
520
|
job_state.old_timestamp = history_data[i + 1].timestamp
|
|
520
521
|
job_state.old_etag = history_data[i + 1].etag
|
|
522
|
+
job_state.old_mime_type = history_data[i + 1].mime_type
|
|
521
523
|
else:
|
|
522
524
|
history_dic_snapshots = {s.data: s for s in history_data[i + 1 : i + 1 + job.compared_versions]}
|
|
523
525
|
close_matches: list[str] = difflib.get_close_matches(
|
|
@@ -527,6 +529,7 @@ class UrlwatchCommand:
|
|
|
527
529
|
job_state.old_data = close_matches[0]
|
|
528
530
|
job_state.old_timestamp = history_dic_snapshots[close_matches[0]].timestamp
|
|
529
531
|
job_state.old_etag = history_dic_snapshots[close_matches[0]].etag
|
|
532
|
+
job_state.old_mime_type = history_dic_snapshots[close_matches[0]].mime_type
|
|
530
533
|
|
|
531
534
|
# TODO: setting of job_state.job.is_markdown = True when it had been set by a filter.
|
|
532
535
|
# Ideally it should be saved as an attribute when saving "data".
|
|
@@ -538,6 +541,7 @@ class UrlwatchCommand:
|
|
|
538
541
|
f'No change (snapshots {-i:2} AND {-(i + 1):2}) with '
|
|
539
542
|
f"'compared_versions: {job.compared_versions}'"
|
|
540
543
|
)
|
|
544
|
+
job_state.verb = 'changed,no_report'
|
|
541
545
|
else:
|
|
542
546
|
label = f'Filtered diff (snapshots {-i:2} and {-(i + 1):2})'
|
|
543
547
|
errorlevel = self.check_test_reporter(job_state, label=label, report=report)
|
|
@@ -360,7 +360,7 @@ class UnifiedDiffer(DifferBase):
|
|
|
360
360
|
diff_text = _unfiltered_diff['text']
|
|
361
361
|
else:
|
|
362
362
|
empty_return: dict[Literal['text', 'markdown', 'html'], str] = {'text': '', 'markdown': '', 'html': ''}
|
|
363
|
-
contextlines = directives.get('context_lines'
|
|
363
|
+
contextlines = directives.get('context_lines', self.job.contextlines)
|
|
364
364
|
if contextlines is None:
|
|
365
365
|
if self.job.additions_only or self.job.deletions_only:
|
|
366
366
|
contextlines = 0
|
|
@@ -565,7 +565,7 @@ class CommandDiffer(DifferBase):
|
|
|
565
565
|
f'Using differ "{directives}"',
|
|
566
566
|
f'--- @ {self.make_timestamp(self.state.old_timestamp, tz)}',
|
|
567
567
|
f'+++ @ {self.make_timestamp(self.state.new_timestamp, tz)}',
|
|
568
|
-
'
|
|
568
|
+
'—' * 37,
|
|
569
569
|
]
|
|
570
570
|
)
|
|
571
571
|
diff = proc.stdout
|
|
@@ -819,7 +819,7 @@ class DeepdiffDiffer(DifferBase):
|
|
|
819
819
|
f'Differ: {self.__kind__} for {data_type}\n'
|
|
820
820
|
f'<span style="color:darkred;">--- @ {self.make_timestamp(self.state.old_timestamp, tz)}</span>\n'
|
|
821
821
|
f'<span style="color:darkgreen;">+++ @ {self.make_timestamp(self.state.new_timestamp, tz)}</span>\n'
|
|
822
|
-
+ '
|
|
822
|
+
+ '—' * 37
|
|
823
823
|
+ '\n'
|
|
824
824
|
+ diff_text[:-1]
|
|
825
825
|
+ '</span>'
|
|
@@ -829,7 +829,7 @@ class DeepdiffDiffer(DifferBase):
|
|
|
829
829
|
text_diff = (
|
|
830
830
|
f'Differ: {self.__kind__} for {data_type}\n'
|
|
831
831
|
f'--- @ {self.make_timestamp(self.state.old_timestamp, tz)}\n'
|
|
832
|
-
f'+++ @ {self.make_timestamp(self.state.new_timestamp, tz)}\n' + '
|
|
832
|
+
f'+++ @ {self.make_timestamp(self.state.new_timestamp, tz)}\n' + '—' * 37 + '\n' + diff_text
|
|
833
833
|
)
|
|
834
834
|
return {'text': text_diff, 'markdown': text_diff}
|
|
835
835
|
|
|
@@ -1010,14 +1010,12 @@ class ImageDiffer(DifferBase):
|
|
|
1010
1010
|
# Prepare HTML output
|
|
1011
1011
|
new_timestamp = self.make_timestamp(self.state.new_timestamp, tz)
|
|
1012
1012
|
old_timestamp = self.make_timestamp(self.state.old_timestamp, tz)
|
|
1013
|
-
span_added = f'<span style="{self.css_added_style}">'
|
|
1014
|
-
# span_deltd = f'<span style="{self.css_deltd_style}">'
|
|
1015
1013
|
htm = [
|
|
1016
|
-
f'Differ: {self.__kind__} for {data_type}',
|
|
1014
|
+
f'<span style="font-family:monospace">Differ: {self.__kind__} for {data_type}',
|
|
1017
1015
|
f'<span style="color:darkred;">--- @ {old_timestamp}{old_data}</span>',
|
|
1018
1016
|
f'<span style="color:darkgreen;">+++ @ {new_timestamp}{new_data}' f'</span>',
|
|
1019
|
-
'
|
|
1020
|
-
|
|
1017
|
+
('—' * 37 + '</span>'),
|
|
1018
|
+
'New image:',
|
|
1021
1019
|
f'<img src="data:image/{new_image.format.lower()};base64,{encoded_new}">',
|
|
1022
1020
|
'Differences from old (in yellow):',
|
|
1023
1021
|
f'<img src="data:image/{old_image.format.lower()};base64,{encoded_diff}">',
|
|
@@ -1042,7 +1040,7 @@ class AIGoogleDiffer(DifferBase):
|
|
|
1042
1040
|
__kind__ = 'ai_google'
|
|
1043
1041
|
|
|
1044
1042
|
__supported_directives__ = {
|
|
1045
|
-
'model': 'model name from https://ai.google.dev/models/gemini (default: gemini-1.5-
|
|
1043
|
+
'model': 'model name from https://ai.google.dev/gemini-api/docs/models/gemini (default: gemini-1.5-flash)',
|
|
1046
1044
|
'prompt': 'a custom prompt - {unified_diff}, {old_data} and {new_data} will be replaced; ask for markdown',
|
|
1047
1045
|
'prompt_ud_context_lines': 'the number of context lines for {unified_diff} (default: 9999)',
|
|
1048
1046
|
'timeout': 'the number of seconds before timing out the API call (default: 300)',
|
|
@@ -1086,27 +1084,28 @@ class AIGoogleDiffer(DifferBase):
|
|
|
1086
1084
|
)
|
|
1087
1085
|
|
|
1088
1086
|
api_version = '1beta'
|
|
1089
|
-
_models_token_limits = { # from https://ai.google.dev/models/gemini
|
|
1090
|
-
'gemini-1.5
|
|
1091
|
-
'gemini-
|
|
1092
|
-
'gemini-
|
|
1093
|
-
'gemini-pro-latest': 30720,
|
|
1094
|
-
'gemini-1.0-pro-001': 30720,
|
|
1087
|
+
_models_token_limits = { # from https://ai.google.dev/gemini-api/docs/models/gemini
|
|
1088
|
+
'gemini-1.5': 1048576,
|
|
1089
|
+
' gemini-1.0': 30720,
|
|
1090
|
+
'gemini-pro': 30720, # legacy
|
|
1095
1091
|
}
|
|
1096
1092
|
|
|
1097
1093
|
if 'model' not in directives:
|
|
1098
|
-
directives['model'] = 'gemini-1.5-
|
|
1094
|
+
directives['model'] = 'gemini-1.5-flash' # also for footer
|
|
1099
1095
|
model = directives['model']
|
|
1100
1096
|
token_limit = directives.get('token_limit')
|
|
1101
1097
|
if not token_limit:
|
|
1102
|
-
|
|
1098
|
+
for _model, _token_limit in _models_token_limits.items():
|
|
1099
|
+
if model.startswith(_model):
|
|
1100
|
+
token_limit = _token_limit
|
|
1101
|
+
break
|
|
1102
|
+
if not token_limit:
|
|
1103
1103
|
logger.error(
|
|
1104
1104
|
f"Job {self.job.index_number}: Differ '{self.__kind__}' does not know `model: {model}` "
|
|
1105
|
-
f"(supported: {', '.join(sorted(list(_models_token_limits.keys())))}) "
|
|
1105
|
+
f"(supported models starting with: {', '.join(sorted(list(_models_token_limits.keys())))}) "
|
|
1106
1106
|
f'({self.job.get_location()})'
|
|
1107
1107
|
)
|
|
1108
1108
|
return f'## ERROR in summarizing the changes using {self.__kind__}:\n' f'Unknown model {model}.\n'
|
|
1109
|
-
token_limit = _models_token_limits[model]
|
|
1110
1109
|
|
|
1111
1110
|
if '{unified_diff}' in prompt:
|
|
1112
1111
|
context_lines = directives.get('prompt_ud_context_lines', 9999)
|
|
@@ -1252,3 +1251,189 @@ class AIGoogleDiffer(DifferBase):
|
|
|
1252
1251
|
+ f'<i><small>{footer}</small></i>'
|
|
1253
1252
|
),
|
|
1254
1253
|
}
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
class WdiffDiffer(DifferBase):
|
|
1257
|
+
__kind__ = 'wdiff'
|
|
1258
|
+
|
|
1259
|
+
__supported_directives__: dict[str, str] = {
|
|
1260
|
+
'context_lines': 'the number of context lines (default: 3)',
|
|
1261
|
+
'range_info': 'include range information lines (default: true)',
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
def differ(
|
|
1265
|
+
self,
|
|
1266
|
+
directives: dict[str, Any],
|
|
1267
|
+
report_kind: Literal['text', 'markdown', 'html'],
|
|
1268
|
+
_unfiltered_diff: Optional[dict[Literal['text', 'markdown', 'html'], str]] = None,
|
|
1269
|
+
tz: Optional[str] = None,
|
|
1270
|
+
) -> dict[Literal['text', 'markdown', 'html'], str]:
|
|
1271
|
+
warnings.warn(
|
|
1272
|
+
f'Job {self.job.index_number}: Differ {self.__kind__} is WORK IN PROGRESS and has KNOWN bugs which '
|
|
1273
|
+
"are being worked on. DO NOT USE AS THE RESULTS WON'T BE CORRECT.",
|
|
1274
|
+
RuntimeWarning,
|
|
1275
|
+
)
|
|
1276
|
+
if not isinstance(self.state.old_data, str):
|
|
1277
|
+
raise ValueError
|
|
1278
|
+
if not isinstance(self.state.new_data, str):
|
|
1279
|
+
raise ValueError
|
|
1280
|
+
|
|
1281
|
+
# Split the texts into words tokenizing newline
|
|
1282
|
+
if self.job.is_markdown:
|
|
1283
|
+
# Don't split spaces in link text, tokenize space as </s>
|
|
1284
|
+
old_data = re.sub(r'\[(.*?)\]', lambda x: '[' + x.group(1).replace(' ', '</s>') + ']', self.state.old_data)
|
|
1285
|
+
words1 = old_data.replace('\n', ' <\\n> ').split(' ')
|
|
1286
|
+
new_data = re.sub(r'\[(.*?)\]', lambda x: '[' + x.group(1).replace(' ', '</s>') + ']', self.state.new_data)
|
|
1287
|
+
words2 = new_data.replace('\n', ' <\\n> ').split(' ')
|
|
1288
|
+
else:
|
|
1289
|
+
words1 = self.state.old_data.replace('\n', ' <\\n> ').split(' ')
|
|
1290
|
+
words2 = self.state.new_data.replace('\n', ' <\\n> ').split(' ')
|
|
1291
|
+
|
|
1292
|
+
# Create a Differ object
|
|
1293
|
+
import difflib
|
|
1294
|
+
|
|
1295
|
+
d = difflib.Differ()
|
|
1296
|
+
|
|
1297
|
+
# Generate a difference list
|
|
1298
|
+
diff = list(d.compare(words1, words2))
|
|
1299
|
+
|
|
1300
|
+
add_html = '<span style="background-color:#d1ffd1;color:#082b08;">'
|
|
1301
|
+
rem_html = '<span style="background-color:#fff0f0;color:#9c1c1c;text-decoration:line-through;">'
|
|
1302
|
+
|
|
1303
|
+
head_text = (
|
|
1304
|
+
'Differ: wdiff\n'
|
|
1305
|
+
f'\033[91m--- @ {self.make_timestamp(self.state.old_timestamp, tz)}\033[0m\n'
|
|
1306
|
+
f'\033[92m+++ @ {self.make_timestamp(self.state.new_timestamp, tz)}\033[0m\n' + '—' * 37 + '\n'
|
|
1307
|
+
)
|
|
1308
|
+
head_html = '<br>\n'.join(
|
|
1309
|
+
[
|
|
1310
|
+
'<span style="font-family:monospace;">Differ: wdiff',
|
|
1311
|
+
f'<span style="color:darkred;">--- @ {self.make_timestamp(self.state.old_timestamp, tz)}</span>',
|
|
1312
|
+
f'<span style="color:darkgreen;">+++ @ {self.make_timestamp(self.state.new_timestamp, tz)}</span>',
|
|
1313
|
+
'—' * 37 + '</span>',
|
|
1314
|
+
'',
|
|
1315
|
+
]
|
|
1316
|
+
)
|
|
1317
|
+
# Process the diff output to make it more wdiff-like
|
|
1318
|
+
result = []
|
|
1319
|
+
result_html = []
|
|
1320
|
+
prev_word_text = ''
|
|
1321
|
+
prev_word_html = ''
|
|
1322
|
+
next_text = ''
|
|
1323
|
+
next_html = ''
|
|
1324
|
+
add = False
|
|
1325
|
+
rem = False
|
|
1326
|
+
for word_text in diff:
|
|
1327
|
+
if len(word_text) < 3:
|
|
1328
|
+
continue
|
|
1329
|
+
if word_text[0] == '?':
|
|
1330
|
+
continue
|
|
1331
|
+
word_html = word_text
|
|
1332
|
+
pre_text = [next_text] if next_text else []
|
|
1333
|
+
pre_html = [next_html] if next_html else []
|
|
1334
|
+
next_text = ''
|
|
1335
|
+
next_html = ''
|
|
1336
|
+
|
|
1337
|
+
if word_text[0] == '+' and not add: # Beginning of additions
|
|
1338
|
+
if rem:
|
|
1339
|
+
prev_word_html += '</span>'
|
|
1340
|
+
rem = False
|
|
1341
|
+
if word_text[2:] == '<\\n>':
|
|
1342
|
+
next_text = '\033[92m'
|
|
1343
|
+
next_html = add_html
|
|
1344
|
+
else:
|
|
1345
|
+
pre_text.append('\033[92m')
|
|
1346
|
+
pre_html.append(add_html)
|
|
1347
|
+
add = True
|
|
1348
|
+
elif word_text[0] == '-' and not rem: # Beginning of deletions
|
|
1349
|
+
if add:
|
|
1350
|
+
prev_word_html += '</span>'
|
|
1351
|
+
add = False
|
|
1352
|
+
if word_text[2:] == '<\\n>':
|
|
1353
|
+
next_text = '\033[91m'
|
|
1354
|
+
next_html = rem_html
|
|
1355
|
+
else:
|
|
1356
|
+
pre_text.append('\033[91m')
|
|
1357
|
+
pre_html.append(rem_html)
|
|
1358
|
+
rem = True
|
|
1359
|
+
elif word_text[0] == ' ' and (add or rem): # Unchanged word
|
|
1360
|
+
if prev_word_text == '<\\n>':
|
|
1361
|
+
prev_word_text = '\033[0m<\\n>'
|
|
1362
|
+
prev_word_html = '</span><\\n>'
|
|
1363
|
+
else:
|
|
1364
|
+
prev_word_text += '\033[0m'
|
|
1365
|
+
prev_word_html += '</span>'
|
|
1366
|
+
add = False
|
|
1367
|
+
rem = False
|
|
1368
|
+
elif word_text[2:] == '<\\n>': # New line
|
|
1369
|
+
if add:
|
|
1370
|
+
word_html = f' </span><\\n> {add_html}'
|
|
1371
|
+
elif rem:
|
|
1372
|
+
word_html = f' </span><\\n> {rem_html}'
|
|
1373
|
+
|
|
1374
|
+
result.append(prev_word_text)
|
|
1375
|
+
result_html.append(prev_word_html)
|
|
1376
|
+
pre_text.append(word_text[2:])
|
|
1377
|
+
pre_html.append(word_html[2:])
|
|
1378
|
+
prev_word_text = ''.join(pre_text)
|
|
1379
|
+
prev_word_html = ''.join(pre_html)
|
|
1380
|
+
result.append(prev_word_text)
|
|
1381
|
+
result_html.append(prev_word_html)
|
|
1382
|
+
if add or rem:
|
|
1383
|
+
result[-1] += '\033[0m'
|
|
1384
|
+
result_html[-1] += '</span>'
|
|
1385
|
+
|
|
1386
|
+
# rebuild the text from words, replacing the newline token
|
|
1387
|
+
diff_text = ' '.join(result[1:]).replace('<\\n> ', '\n').replace('<\\n>', '\n')
|
|
1388
|
+
diff_html = ' '.join(result_html[1:]).replace('<\\n> ', '\n').replace('<\\n>', '\n')
|
|
1389
|
+
|
|
1390
|
+
# build contextlines
|
|
1391
|
+
contextlines = directives.get('context_lines', self.job.contextlines)
|
|
1392
|
+
# contextlines = 999
|
|
1393
|
+
if contextlines is None:
|
|
1394
|
+
contextlines = 3
|
|
1395
|
+
range_info = directives.get('range_info', True)
|
|
1396
|
+
if contextlines < len(diff_text.splitlines()):
|
|
1397
|
+
lines_with_changes = []
|
|
1398
|
+
for i, line in enumerate(diff_text.splitlines()):
|
|
1399
|
+
if '\033[9' in line:
|
|
1400
|
+
lines_with_changes.append(i)
|
|
1401
|
+
if contextlines:
|
|
1402
|
+
lines_to_keep: set[int] = set()
|
|
1403
|
+
for i in lines_with_changes:
|
|
1404
|
+
lines_to_keep.update(r for r in range(i - contextlines, i + contextlines + 1))
|
|
1405
|
+
else:
|
|
1406
|
+
lines_to_keep = set(lines_with_changes)
|
|
1407
|
+
new_diff_text = []
|
|
1408
|
+
new_diff_html = []
|
|
1409
|
+
last_line = 0
|
|
1410
|
+
skip = False
|
|
1411
|
+
for i, (line_text, line_html) in enumerate(zip(diff_text.splitlines(), diff_html.splitlines())):
|
|
1412
|
+
if i in lines_to_keep:
|
|
1413
|
+
if range_info and skip:
|
|
1414
|
+
new_diff_text.append(f'@@ {last_line}...{i + 1} @@')
|
|
1415
|
+
new_diff_html.append(f'@@ {last_line}...{i + 1} @@')
|
|
1416
|
+
skip = False
|
|
1417
|
+
new_diff_text.append(line_text)
|
|
1418
|
+
new_diff_html.append(line_html)
|
|
1419
|
+
last_line = i + 1
|
|
1420
|
+
else:
|
|
1421
|
+
skip = True
|
|
1422
|
+
diff_text = '\n'.join(new_diff_text)
|
|
1423
|
+
diff_html = '\n'.join(new_diff_html)
|
|
1424
|
+
|
|
1425
|
+
if self.job.is_markdown:
|
|
1426
|
+
diff_text = diff_text.replace('</s>', ' ')
|
|
1427
|
+
diff_html = diff_html.replace('</s>', ' ')
|
|
1428
|
+
diff_html = mark_to_html(diff_html, self.job.markdown_padded_tables).replace('<p>', '').replace('</p>', '')
|
|
1429
|
+
|
|
1430
|
+
if self.job.monospace:
|
|
1431
|
+
diff_html = f'<span style="font-family:monospace;white-space:pre-wrap">{diff_html}</span>'
|
|
1432
|
+
else:
|
|
1433
|
+
diff_html = diff_html.replace('\n', '<br>\n')
|
|
1434
|
+
|
|
1435
|
+
return {
|
|
1436
|
+
'text': head_text + diff_text,
|
|
1437
|
+
'markdown': head_text + diff_text,
|
|
1438
|
+
'html': head_html + diff_html,
|
|
1439
|
+
}
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import base64
|
|
7
8
|
import getpass
|
|
8
9
|
import logging
|
|
10
|
+
import re
|
|
9
11
|
import smtplib
|
|
10
12
|
import subprocess # noqa: S404 Consider possible security implications associated with the subprocess module.
|
|
11
13
|
from dataclasses import dataclass
|
|
@@ -14,7 +16,7 @@ from email.message import EmailMessage
|
|
|
14
16
|
from email.utils import formatdate
|
|
15
17
|
from pathlib import Path
|
|
16
18
|
from types import ModuleType
|
|
17
|
-
from typing import Optional, Union
|
|
19
|
+
from typing import Dict, List, Optional, Union
|
|
18
20
|
|
|
19
21
|
try:
|
|
20
22
|
import keyring
|
|
@@ -47,6 +49,33 @@ class Mailer:
|
|
|
47
49
|
:param text_body: The body in text format
|
|
48
50
|
:param html_body: The body in html format (optional)
|
|
49
51
|
"""
|
|
52
|
+
|
|
53
|
+
def extract_inline_images(html_body: str) -> tuple[str, Dict[str, bytes]]:
|
|
54
|
+
"""Extract inline images from the email.
|
|
55
|
+
|
|
56
|
+
:param html_body: The HTML with inline images.
|
|
57
|
+
|
|
58
|
+
:return: The HTML with src tags and a dictionary of cid and file.
|
|
59
|
+
"""
|
|
60
|
+
cid_dict: Dict[str, bytes] = {}
|
|
61
|
+
cid_counter = 1
|
|
62
|
+
|
|
63
|
+
def replace_img(match: re.Match) -> str:
|
|
64
|
+
"""Function to replace the matched img tags with src="cid:<...>"> and to add the cid and the image to
|
|
65
|
+
the cid_dict object.
|
|
66
|
+
"""
|
|
67
|
+
nonlocal cid_counter
|
|
68
|
+
image_format, image_data_b64 = match.groups()
|
|
69
|
+
image_data = base64.b64decode(image_data_b64)
|
|
70
|
+
image_cid = f'image{cid_counter}_{image_format.split(";")[0]}'
|
|
71
|
+
cid_dict[image_cid] = image_data
|
|
72
|
+
new_img_tag = f'<img src="cid:{image_cid}">'
|
|
73
|
+
cid_counter += 1
|
|
74
|
+
return new_img_tag
|
|
75
|
+
|
|
76
|
+
edited_html = re.sub(r'<img src="data:image/(.+?);base64,(.+?)"', replace_img, html_body)
|
|
77
|
+
return edited_html, cid_dict
|
|
78
|
+
|
|
50
79
|
msg = EmailMessage(policy=policy.SMTPUTF8)
|
|
51
80
|
msg['From'] = from_email
|
|
52
81
|
msg['To'] = to_email
|
|
@@ -54,8 +83,21 @@ class Mailer:
|
|
|
54
83
|
msg['Date'] = formatdate(localtime=True)
|
|
55
84
|
msg.set_content(text_body, subtype='plain')
|
|
56
85
|
if html_body is not None:
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
if ';base64,' not in html_body:
|
|
87
|
+
msg.add_alternative(html_body, subtype='html')
|
|
88
|
+
else:
|
|
89
|
+
html_body, cid_dict = extract_inline_images(html_body)
|
|
90
|
+
msg.add_alternative(html_body, subtype='html')
|
|
91
|
+
payloads: List[EmailMessage] = msg.get_payload()[1] # type: ignore[assignment,index]
|
|
92
|
+
for image_cid, image_data in cid_dict.items():
|
|
93
|
+
payloads[1].add_related(
|
|
94
|
+
image_data,
|
|
95
|
+
maintype='image',
|
|
96
|
+
subtype=image_cid.split('_')[-1],
|
|
97
|
+
disposition='inline',
|
|
98
|
+
filename=image_cid,
|
|
99
|
+
cid=f'<{image_cid}>',
|
|
100
|
+
)
|
|
59
101
|
return msg
|
|
60
102
|
|
|
61
103
|
|
|
@@ -342,7 +342,7 @@ class HtmlReporter(ReporterBase):
|
|
|
342
342
|
' color-scheme: light dark;\n'
|
|
343
343
|
' supported-color-schemes: light dark;\n'
|
|
344
344
|
' }\n'
|
|
345
|
-
'</style
|
|
345
|
+
'</style>\n'
|
|
346
346
|
'<body style="font-family: Roboto, Arial, Helvetica, sans-serif; font-size: 13px;">\n'
|
|
347
347
|
)
|
|
348
348
|
|
|
@@ -396,7 +396,7 @@ def mark_to_html(
|
|
|
396
396
|
if 'tables' in markdowner_extras:
|
|
397
397
|
html_out = html_out.replace('<table>', '<table border="1" cellspacing="0">')
|
|
398
398
|
# remove <p> tags wrapping
|
|
399
|
-
html_out, sub = re.subn(r'^<p>|</p>$', '', html_out)
|
|
399
|
+
html_out, sub = re.subn(r'^<p>|</p>$', '', html_out) # remove paragraph tags
|
|
400
400
|
if sub:
|
|
401
401
|
return pre + html_out + post
|
|
402
402
|
html_out = re.sub(r'<(/?)h\d>', r'<\g<1>strong>', html_out) # replace heading tags with <strong>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: webchanges
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.23.0
|
|
4
4
|
Summary: Check web (or command output) for changes since last run and notify. Anonymously alerts you of web changes, with
|
|
5
5
|
Author-email: Mike Borsetti <mike+webchanges@borsetti.com>
|
|
6
6
|
Maintainer-email: Mike Borsetti <mike+webchanges@borsetti.com>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|