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.
Files changed (32) hide show
  1. {webchanges-3.22/webchanges.egg-info → webchanges-3.23.0}/PKG-INFO +1 -1
  2. {webchanges-3.22 → webchanges-3.23.0}/webchanges/__init__.py +1 -1
  3. {webchanges-3.22 → webchanges-3.23.0}/webchanges/command.py +4 -0
  4. {webchanges-3.22 → webchanges-3.23.0}/webchanges/differs.py +205 -20
  5. {webchanges-3.22 → webchanges-3.23.0}/webchanges/mailer.py +45 -3
  6. {webchanges-3.22 → webchanges-3.23.0}/webchanges/reporters.py +1 -1
  7. {webchanges-3.22 → webchanges-3.23.0}/webchanges/util.py +1 -1
  8. {webchanges-3.22 → webchanges-3.23.0/webchanges.egg-info}/PKG-INFO +1 -1
  9. {webchanges-3.22 → webchanges-3.23.0}/LICENSE +0 -0
  10. {webchanges-3.22 → webchanges-3.23.0}/MANIFEST.in +0 -0
  11. {webchanges-3.22 → webchanges-3.23.0}/README.rst +0 -0
  12. {webchanges-3.22 → webchanges-3.23.0}/pyproject.toml +0 -0
  13. {webchanges-3.22 → webchanges-3.23.0}/requirements.txt +0 -0
  14. {webchanges-3.22 → webchanges-3.23.0}/setup.cfg +0 -0
  15. {webchanges-3.22 → webchanges-3.23.0}/webchanges/_vendored/__init__.py +0 -0
  16. {webchanges-3.22 → webchanges-3.23.0}/webchanges/_vendored/case_insensitive_dict.py +0 -0
  17. {webchanges-3.22 → webchanges-3.23.0}/webchanges/_vendored/packaging_version.py +0 -0
  18. {webchanges-3.22 → webchanges-3.23.0}/webchanges/cli.py +0 -0
  19. {webchanges-3.22 → webchanges-3.23.0}/webchanges/config.py +0 -0
  20. {webchanges-3.22 → webchanges-3.23.0}/webchanges/filters.py +0 -0
  21. {webchanges-3.22 → webchanges-3.23.0}/webchanges/handler.py +0 -0
  22. {webchanges-3.22 → webchanges-3.23.0}/webchanges/jobs.py +0 -0
  23. {webchanges-3.22 → webchanges-3.23.0}/webchanges/main.py +0 -0
  24. {webchanges-3.22 → webchanges-3.23.0}/webchanges/py.typed +0 -0
  25. {webchanges-3.22 → webchanges-3.23.0}/webchanges/storage.py +0 -0
  26. {webchanges-3.22 → webchanges-3.23.0}/webchanges/storage_minidb.py +0 -0
  27. {webchanges-3.22 → webchanges-3.23.0}/webchanges/worker.py +0 -0
  28. {webchanges-3.22 → webchanges-3.23.0}/webchanges.egg-info/SOURCES.txt +0 -0
  29. {webchanges-3.22 → webchanges-3.23.0}/webchanges.egg-info/dependency_links.txt +0 -0
  30. {webchanges-3.22 → webchanges-3.23.0}/webchanges.egg-info/entry_points.txt +0 -0
  31. {webchanges-3.22 → webchanges-3.23.0}/webchanges.egg-info/requires.txt +0 -0
  32. {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.22
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.22'
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') or self.job.contextlines
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
- '-' * 36,
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
- + '-' * 36
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' + '-' * 36 + '\n' + diff_text
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
- '-' * 36,
1020
- f'{span_added}New:</span>',
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-pro-latest)',
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-pro-latest': 1048576,
1091
- 'gemini-pro': 30720,
1092
- 'gemini-1.0-pro-latest': 30720,
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-pro-latest' # also for footer
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
- if model not in _models_token_limits:
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
- msg.add_alternative(html_body, subtype='html')
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.22
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