webchanges 3.28.0rc0__tar.gz → 3.28.2__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 (33) hide show
  1. {webchanges-3.28.0rc0/webchanges.egg-info → webchanges-3.28.2}/PKG-INFO +24 -14
  2. {webchanges-3.28.0rc0 → webchanges-3.28.2}/README.rst +23 -13
  3. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/__init__.py +1 -1
  4. webchanges-3.28.2/webchanges/__main__.py +10 -0
  5. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/cli.py +11 -7
  6. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/command.py +2 -2
  7. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/differs.py +76 -53
  8. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/filters.py +5 -2
  9. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/handler.py +3 -3
  10. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/jobs.py +4 -7
  11. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/reporters.py +1 -1
  12. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/storage.py +6 -2
  13. {webchanges-3.28.0rc0 → webchanges-3.28.2/webchanges.egg-info}/PKG-INFO +24 -14
  14. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges.egg-info/SOURCES.txt +1 -0
  15. {webchanges-3.28.0rc0 → webchanges-3.28.2}/LICENSE +0 -0
  16. {webchanges-3.28.0rc0 → webchanges-3.28.2}/MANIFEST.in +0 -0
  17. {webchanges-3.28.0rc0 → webchanges-3.28.2}/pyproject.toml +0 -0
  18. {webchanges-3.28.0rc0 → webchanges-3.28.2}/requirements.txt +0 -0
  19. {webchanges-3.28.0rc0 → webchanges-3.28.2}/setup.cfg +0 -0
  20. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/_vendored/__init__.py +0 -0
  21. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/_vendored/headers.py +0 -0
  22. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/_vendored/packaging_version.py +0 -0
  23. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/config.py +0 -0
  24. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/mailer.py +0 -0
  25. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/main.py +0 -0
  26. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/py.typed +0 -0
  27. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/storage_minidb.py +0 -0
  28. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/util.py +0 -0
  29. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/worker.py +0 -0
  30. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges.egg-info/dependency_links.txt +0 -0
  31. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges.egg-info/entry_points.txt +0 -0
  32. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges.egg-info/requires.txt +0 -0
  33. {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: webchanges
3
- Version: 3.28.0rc0
3
+ Version: 3.28.2
4
4
  Summary: Web Changes Delivered. AI-Summarized. Totally Anonymous.
5
5
  Author-email: Mike Borsetti <mike+webchanges@borsetti.com>
6
6
  Maintainer-email: Mike Borsetti <mike+webchanges@borsetti.com>
@@ -195,7 +195,9 @@ For Generative AI summaries (BETA), you need a free `API Key from Google Cloud A
195
195
 
196
196
  Installation
197
197
  ============
198
- Install **webchanges** |pypi_version| |format| |status| |security| with:
198
+ |pypi_version| |format| |status| |security|
199
+
200
+ Install **webchanges** with:
199
201
 
200
202
  .. code-block:: bash
201
203
 
@@ -203,7 +205,7 @@ Install **webchanges** |pypi_version| |format| |status| |security| with:
203
205
 
204
206
  Running in Docker
205
207
  -----------------
206
- **webchanges** can easily run in a container; you can find a `Docker <https://www.docker.com/>`__ implementation
208
+ **webchanges** can easily run in a container and you will find a `Docker <https://www.docker.com/>`__ implementation
207
209
  `here <https://github.com/yubiuser/webchanges-docker>`__.
208
210
 
209
211
 
@@ -247,20 +249,19 @@ Schedule
247
249
  --------
248
250
  **webchanges** leverages the power of a system scheduler:
249
251
 
250
- - On Linux you can use cron, and a tool like `crontab.guru <https://crontab.guru>`__ can build a
251
- schedule expression for you (note: see `here <https://www.computerhope.com/unix/ucrontab.htm>`__ if you have never
252
- used cron before);
252
+ - On Linux you can use cron, with the help of a tool like `crontab.guru <https://crontab.guru>`__ (help `here
253
+ <https://www.computerhope.com/unix/ucrontab.htm>`__);
253
254
  - On Windows you can use `Windows Task Scheduler <https://en.wikipedia.org/wiki/Windows_Task_Scheduler>`__;
254
255
  - On macOS you can use `launchd <https://developer.apple
255
- .com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html>`__ (note: see `here
256
- <https://launchd.info/>`__ if you have never used launchd before).
256
+ .com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html>`__ (help `here
257
+ <https://launchd.info/>`__).
257
258
 
258
259
 
259
260
  Code
260
261
  ====
261
- |coveralls| |issues|
262
+ |coveralls| |issues| |code_style|
262
263
 
263
- The code and issues tracker are hosted on `GitHub <https://github.com/mborsetti/webchanges>`__.
264
+ The code, issues tracker, and discussions are hosted on `GitHub <https://github.com/mborsetti/webchanges>`__.
264
265
 
265
266
 
266
267
  Contributing
@@ -344,7 +345,7 @@ Example enhancements to HTML reporting:
344
345
  .. |format| image:: https://img.shields.io/pypi/format/webchanges.svg
345
346
  :target: https://pypi.org/project/webchanges/
346
347
  :alt: Kit format
347
- .. |downloads| image:: https://static.pepy.tech/badge/webchanges
348
+ .. |downloads| image:: https://img.shields.io/pypi/dm/webchanges.svg
348
349
  :target: https://www.pepy.tech/project/webchanges
349
350
  :alt: PyPI downloads
350
351
  .. |license| image:: https://img.shields.io/pypi/l/webchanges.svg
@@ -356,15 +357,24 @@ Example enhancements to HTML reporting:
356
357
  .. |readthedocs| image:: https://img.shields.io/readthedocs/webchanges/stable.svg?label=
357
358
  :target: https://webchanges.readthedocs.io/
358
359
  :alt: Documentation status
359
- .. |CI| image:: https://github.com/mborsetti/webchanges/actions/workflows/ci-cd.yaml/badge.svg?event=push
360
+ .. |old_CI| image:: https://github.com/mborsetti/webchanges/actions/workflows/ci-cd.yaml/badge.svg?event=push
361
+ :target: https://github.com/mborsetti/webchanges/actions
362
+ :alt: CI testing status
363
+ .. |CI| image:: https://img.shields.io/github/check-runs/mborsetti/webchanges/main
360
364
  :target: https://github.com/mborsetti/webchanges/actions
361
365
  :alt: CI testing status
362
- .. |coveralls| image:: https://coveralls.io/repos/github/mborsetti/webchanges/badge.svg?branch=main
366
+ .. |old_coveralls| image:: https://coveralls.io/repos/github/mborsetti/webchanges/badge.svg?branch=main
367
+ :target: https://coveralls.io/github/mborsetti/webchanges?branch=main
368
+ :alt: Code coverage by Coveralls
369
+ .. |coveralls| image:: https://img.shields.io/coverallsCoverage/github/mborsetti/webchanges.svg
363
370
  :target: https://coveralls.io/github/mborsetti/webchanges?branch=main
364
371
  :alt: Code coverage by Coveralls
372
+ .. |code_style| image:: https://img.shields.io/badge/code%20style-black-000000.svg
373
+ :target: https://github.com/psf/black
374
+ :alt: Code style black
365
375
  .. |status| image:: https://img.shields.io/pypi/status/webchanges.svg
366
376
  :target: https://pypi.org/project/webchanges/
367
377
  :alt: Package stability
368
- .. |security| image:: https://img.shields.io/badge/security-bandit-yellow.svg
378
+ .. |security| image:: https://img.shields.io/badge/security-bandit-green.svg
369
379
  :target: https://github.com/PyCQA/bandit
370
380
  :alt: Security Status
@@ -29,7 +29,9 @@ For Generative AI summaries (BETA), you need a free `API Key from Google Cloud A
29
29
 
30
30
  Installation
31
31
  ============
32
- Install **webchanges** |pypi_version| |format| |status| |security| with:
32
+ |pypi_version| |format| |status| |security|
33
+
34
+ Install **webchanges** with:
33
35
 
34
36
  .. code-block:: bash
35
37
 
@@ -37,7 +39,7 @@ Install **webchanges** |pypi_version| |format| |status| |security| with:
37
39
 
38
40
  Running in Docker
39
41
  -----------------
40
- **webchanges** can easily run in a container; you can find a `Docker <https://www.docker.com/>`__ implementation
42
+ **webchanges** can easily run in a container and you will find a `Docker <https://www.docker.com/>`__ implementation
41
43
  `here <https://github.com/yubiuser/webchanges-docker>`__.
42
44
 
43
45
 
@@ -81,20 +83,19 @@ Schedule
81
83
  --------
82
84
  **webchanges** leverages the power of a system scheduler:
83
85
 
84
- - On Linux you can use cron, and a tool like `crontab.guru <https://crontab.guru>`__ can build a
85
- schedule expression for you (note: see `here <https://www.computerhope.com/unix/ucrontab.htm>`__ if you have never
86
- used cron before);
86
+ - On Linux you can use cron, with the help of a tool like `crontab.guru <https://crontab.guru>`__ (help `here
87
+ <https://www.computerhope.com/unix/ucrontab.htm>`__);
87
88
  - On Windows you can use `Windows Task Scheduler <https://en.wikipedia.org/wiki/Windows_Task_Scheduler>`__;
88
89
  - On macOS you can use `launchd <https://developer.apple
89
- .com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html>`__ (note: see `here
90
- <https://launchd.info/>`__ if you have never used launchd before).
90
+ .com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html>`__ (help `here
91
+ <https://launchd.info/>`__).
91
92
 
92
93
 
93
94
  Code
94
95
  ====
95
- |coveralls| |issues|
96
+ |coveralls| |issues| |code_style|
96
97
 
97
- The code and issues tracker are hosted on `GitHub <https://github.com/mborsetti/webchanges>`__.
98
+ The code, issues tracker, and discussions are hosted on `GitHub <https://github.com/mborsetti/webchanges>`__.
98
99
 
99
100
 
100
101
  Contributing
@@ -178,7 +179,7 @@ Example enhancements to HTML reporting:
178
179
  .. |format| image:: https://img.shields.io/pypi/format/webchanges.svg
179
180
  :target: https://pypi.org/project/webchanges/
180
181
  :alt: Kit format
181
- .. |downloads| image:: https://static.pepy.tech/badge/webchanges
182
+ .. |downloads| image:: https://img.shields.io/pypi/dm/webchanges.svg
182
183
  :target: https://www.pepy.tech/project/webchanges
183
184
  :alt: PyPI downloads
184
185
  .. |license| image:: https://img.shields.io/pypi/l/webchanges.svg
@@ -190,15 +191,24 @@ Example enhancements to HTML reporting:
190
191
  .. |readthedocs| image:: https://img.shields.io/readthedocs/webchanges/stable.svg?label=
191
192
  :target: https://webchanges.readthedocs.io/
192
193
  :alt: Documentation status
193
- .. |CI| image:: https://github.com/mborsetti/webchanges/actions/workflows/ci-cd.yaml/badge.svg?event=push
194
+ .. |old_CI| image:: https://github.com/mborsetti/webchanges/actions/workflows/ci-cd.yaml/badge.svg?event=push
195
+ :target: https://github.com/mborsetti/webchanges/actions
196
+ :alt: CI testing status
197
+ .. |CI| image:: https://img.shields.io/github/check-runs/mborsetti/webchanges/main
194
198
  :target: https://github.com/mborsetti/webchanges/actions
195
199
  :alt: CI testing status
196
- .. |coveralls| image:: https://coveralls.io/repos/github/mborsetti/webchanges/badge.svg?branch=main
200
+ .. |old_coveralls| image:: https://coveralls.io/repos/github/mborsetti/webchanges/badge.svg?branch=main
201
+ :target: https://coveralls.io/github/mborsetti/webchanges?branch=main
202
+ :alt: Code coverage by Coveralls
203
+ .. |coveralls| image:: https://img.shields.io/coverallsCoverage/github/mborsetti/webchanges.svg
197
204
  :target: https://coveralls.io/github/mborsetti/webchanges?branch=main
198
205
  :alt: Code coverage by Coveralls
206
+ .. |code_style| image:: https://img.shields.io/badge/code%20style-black-000000.svg
207
+ :target: https://github.com/psf/black
208
+ :alt: Code style black
199
209
  .. |status| image:: https://img.shields.io/pypi/status/webchanges.svg
200
210
  :target: https://pypi.org/project/webchanges/
201
211
  :alt: Package stability
202
- .. |security| image:: https://img.shields.io/badge/security-bandit-yellow.svg
212
+ .. |security| image:: https://img.shields.io/badge/security-bandit-green.svg
203
213
  :target: https://github.com/PyCQA/bandit
204
214
  :alt: Security Status
@@ -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.28.0rc0'
25
+ __version__ = '3.28.2'
26
26
  __description__ = (
27
27
  'Check web (or command output) for changes since last run and notify.\n'
28
28
  '\n'
@@ -0,0 +1,10 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ parent_dir = Path(__file__).parent.parent
5
+ sys.path.insert(1, str(parent_dir))
6
+
7
+ if __name__ == '__main__':
8
+ from cli import main
9
+
10
+ main()
@@ -244,19 +244,18 @@ def first_run(command_config: CommandConfig) -> None:
244
244
  def load_hooks(hooks_file: Path) -> None:
245
245
  """Load hooks file."""
246
246
  if not hooks_file.is_file():
247
- warnings.warn(
248
- f'Hooks file not imported because {hooks_file} is not a file',
249
- ImportWarning,
250
- )
247
+ # do not use ImportWarning as it could be suppressed
248
+ warnings.warn(f'Hooks file not imported because {hooks_file} is not a file', RuntimeWarning)
251
249
  return
252
250
 
253
251
  hooks_file_errors = file_ownership_checks(hooks_file)
254
252
  if hooks_file_errors:
253
+ logger.debug('Here should come the warning')
254
+ # do not use ImportWarning as it could be suppressed
255
255
  warnings.warn(
256
- f'Hooks file {hooks_file} not imported because '
257
- f" {' and '.join(hooks_file_errors)}.\n"
256
+ f"Hooks file {hooks_file} not not imported because{' and '.join(hooks_file_errors)}.\n"
258
257
  f'(see {__docs_url__}en/stable/hooks.html#important-note-for-hooks-file)',
259
- ImportWarning,
258
+ RuntimeWarning,
260
259
  )
261
260
  else:
262
261
  logger.info(f'Importing hooks module from {hooks_file}')
@@ -372,6 +371,10 @@ def main() -> None: # pragma: no cover
372
371
  # Set up the logger to verbose if needed
373
372
  setup_logger(command_config.verbose, command_config.log_file)
374
373
 
374
+ # log defaults
375
+ logger.debug(f'Default config path is {config_path}')
376
+ logger.debug(f'Default data path is {data_path}')
377
+
375
378
  # For speed, run these here
376
379
  handle_unitialized_actions(command_config)
377
380
 
@@ -401,6 +404,7 @@ def main() -> None: # pragma: no cover
401
404
 
402
405
  # load config (which for syntax checking requires hooks to be loaded too)
403
406
  if command_config.hooks_files:
407
+ logger.debug(f'Hooks files to be loaded: {command_config.hooks_files}')
404
408
  for hooks_file in command_config.hooks_files:
405
409
  load_hooks(hooks_file)
406
410
  config_storage.load()
@@ -498,8 +498,8 @@ class UrlwatchCommand:
498
498
 
499
499
  def test_differ(self, arg_test_differ: list[str]) -> int:
500
500
  """
501
- Runs diffs for a job on all the saved snapshots and outputs the result to stdout or whatever reporter is
502
- selected with --test-reporter.
501
+ Runs diffs for a job on all the saved snapshots and outputs the result to stdout or the reporter selected
502
+ with --test-reporter.
503
503
 
504
504
  :param arg_test_differ: Either the job_id or a list containing [job_id, max_diffs]
505
505
  :return: 1 if error, 0 if successful.
@@ -991,6 +991,7 @@ class ImageDiffer(DifferBase):
991
991
  def ai_google(
992
992
  old_image: Image.Image,
993
993
  new_image: Image.Image,
994
+ diff_image: Image.Image,
994
995
  directives: AiGoogleDirectives,
995
996
  ) -> str:
996
997
  """Summarize changes in image using Generative AI (ALPHA)."""
@@ -1073,10 +1074,6 @@ class ImageDiffer(DifferBase):
1073
1074
  }
1074
1075
  }
1075
1076
 
1076
- # Create a diff image
1077
- # diff_image = ImageChops.difference(old_image, new_image)
1078
- # diff_image.format = new_image.format
1079
-
1080
1077
  # upload to Google
1081
1078
  additional_parts: list[dict[str, dict[str, str]]] = []
1082
1079
  executor = ThreadPoolExecutor()
@@ -1085,7 +1082,7 @@ class ImageDiffer(DifferBase):
1085
1082
  (
1086
1083
  ('old image', old_image),
1087
1084
  ('new image', new_image),
1088
- # ('differences image', diff_image),
1085
+ ('differences image', diff_image),
1089
1086
  ),
1090
1087
  ):
1091
1088
  if 'error' not in additional_part:
@@ -1107,22 +1104,33 @@ class ImageDiffer(DifferBase):
1107
1104
  'focus on the most significant changes.'
1108
1105
  )
1109
1106
  model_prompt = (
1107
+ 'You are a skilled visual analyst tasked with analyzing two versions of an image and summarizing the '
1108
+ 'key differences between them. The audience for your summary is already familiar with the '
1109
+ "image's content, so you should focus only on the most significant differences.\n\n"
1110
1110
  '**Instructions:**\n\n'
1111
- f"1. Compare the new image {additional_parts[1]['file_data']['file_uri']} to the old image "
1112
- f"{additional_parts[0]['file_data']['file_uri']}, focusing only on major changes.\n"
1113
- '2. Summarize the meaning of the changes in a clear and concise manner. Be specific.\n'
1114
- '3. Use Markdown formatting to structure your summary effectively. Use headings, bullet points, and '
1115
- 'other Markdown elements as needed to enhance readability.\n'
1116
- '4. Restrict your analysis and summary to these two specific images. Do not introduce external '
1117
- 'information or assumptions. Do not introduce knowledge from images you may have already seen.\n'
1111
+ '1. Carefully examine the yellow areas in the image '
1112
+ f"{additional_parts[2]['file_data']['file_uri']}, identify the differences, and describe them.\n"
1113
+ f"2. Refer to the old version of the image {additional_parts[0]['file_data']['file_uri']} and the new "
1114
+ f" version {additional_parts[1]['file_data']['file_uri']}.\n"
1115
+ '3. You are only interested in those differences, such as additions, removals, or alterations, that '
1116
+ 'modify the intended message or interpretation.\n'
1117
+ '4. Summarize the identified differences, except those ignored, in a clear and concise manner, '
1118
+ 'explaining how the meaning has shifted or evolved in the new version compared to the old version only '
1119
+ 'when necessary. Be specific and provide examples to illustrate your points when needed.\n'
1120
+ '5. If there are only additions to the image, then summarize the additions.\n'
1121
+ '6. Use Markdown formatting to structure your summary effectively. Use headings, bullet points, '
1122
+ 'and other Markdown elements as needed to enhance readability.\n'
1123
+ '7. Restrict your analysis and summary to the information provided within these images. Do '
1124
+ 'not introduce external information or assumptions.\n'
1118
1125
  )
1119
- summary = AIGoogleDiffer._send_to_model(
1126
+ summary, _ = AIGoogleDiffer._send_to_model(
1120
1127
  self.job,
1121
1128
  system_instructions,
1122
1129
  model_prompt,
1123
1130
  additional_parts=additional_parts, # type: ignore[arg-type]
1124
1131
  directives=directives,
1125
1132
  )
1133
+
1126
1134
  return summary
1127
1135
 
1128
1136
  data_type = directives.get('data_type', 'url')
@@ -1134,8 +1142,8 @@ class ImageDiffer(DifferBase):
1134
1142
  if data_type == 'url':
1135
1143
  old_image = load_image_from_web(self.state.old_data)
1136
1144
  new_image = load_image_from_web(self.state.new_data)
1137
- old_data = f' (<a href="{self.state.old_data}">Old image</a>)'
1138
- new_data = f' (<a href="{self.state.new_data}">New image</a>)'
1145
+ old_data = f' (<a href="{self.state.old_data}" target="_blank">Old image</a>)'
1146
+ new_data = f' (<a href="{self.state.new_data}" target="_blank">New image</a>)'
1139
1147
  elif data_type == 'ascii85':
1140
1148
  old_image = load_image_from_ascii85(self.state.old_data)
1141
1149
  new_image = load_image_from_ascii85(self.state.new_data)
@@ -1149,8 +1157,8 @@ class ImageDiffer(DifferBase):
1149
1157
  else: # 'filename'
1150
1158
  old_image = load_image_from_file(self.state.old_data)
1151
1159
  new_image = load_image_from_file(self.state.new_data)
1152
- old_data = f' (<a href="file://{self.state.old_data}">Old image</a>)'
1153
- new_data = f' (<a href="file://{self.state.new_data}">New image</a>)'
1160
+ old_data = f' (<a href="file://{self.state.old_data}" target="_blank">Old image</a>)'
1161
+ new_data = f' (<a href="file://{self.state.new_data}" target="_blank">New image</a>)'
1154
1162
 
1155
1163
  # Check formats TODO: is it needed? under which circumstances?
1156
1164
  # if new_image.format != old_image.format:
@@ -1202,7 +1210,7 @@ class ImageDiffer(DifferBase):
1202
1210
  # prepare AI summary
1203
1211
  summary = ''
1204
1212
  if 'ai_google' in directives:
1205
- summary = ai_google(old_image, new_image, directives.get('ai_google', {}))
1213
+ summary = ai_google(old_image, new_image, diff_image, directives.get('ai_google', {}))
1206
1214
 
1207
1215
  # Prepare HTML output
1208
1216
  htm = [
@@ -1246,8 +1254,14 @@ class ImageDiffer(DifferBase):
1246
1254
  )
1247
1255
  footer = f'Summary generated by Google Generative AI (ai_google directive(s): {directives_text})'
1248
1256
  return {
1249
- 'text': f'{summary}\n\n\nA visualization is available in HTML reports.\n------------\n{footer}',
1250
- 'markdown': f'{summary}\n\n\nA visualization is available in HTML reports.\n* * *\n{footer}',
1257
+ 'text': (
1258
+ f'{summary}\n\n\nA visualization of differences is available in {__package__} HTML reports.'
1259
+ f'\n------------\n{footer}'
1260
+ ),
1261
+ 'markdown': (
1262
+ f'{summary}\n\n\nA visualization of differences is available in {__package__} HTML reports.'
1263
+ f'\n* * *\n{footer}'
1264
+ ),
1251
1265
  'html': '<br>\n'.join(
1252
1266
  [
1253
1267
  mark_to_html(summary, extras={'tables'}).replace('<h2>', '<h3>').replace('</h2>', '</h3>'),
@@ -1273,9 +1287,7 @@ class AIGoogleDiffer(DifferBase):
1273
1287
  __kind__ = 'ai_google'
1274
1288
 
1275
1289
  __supported_directives__ = {
1276
- 'model': (
1277
- 'model name from https://ai.google.dev/gemini-api/docs/models/gemini (default: gemini-1.5-flash-latest)'
1278
- ),
1290
+ 'model': ('model name from https://ai.google.dev/gemini-api/docs/models/gemini (default: gemini-2.0-flash)'),
1279
1291
  'system_instructions': (
1280
1292
  'Optional tone and style instructions for the model (default: see documentation at'
1281
1293
  'https://webchanges.readthedocs.io/en/stable/differs.html#ai-google-diff)'
@@ -1300,20 +1312,17 @@ class AIGoogleDiffer(DifferBase):
1300
1312
  model_prompt: str,
1301
1313
  additional_parts: list[dict[str, str | dict[str, str]]] | None = None,
1302
1314
  directives: AiGoogleDirectives | None = None,
1303
- ) -> str:
1304
- """Creates the summary request to the model"""
1315
+ ) -> tuple[str, str]:
1316
+ """Creates the summary request to the model; returns the summary and the version of the actual model used."""
1305
1317
  api_version = '1beta'
1306
1318
  if directives is None:
1307
1319
  directives = {}
1308
- if 'model' not in directives:
1309
- directives['model'] = 'gemini-1.5-pro' # also for footer
1310
- model = directives.get('model')
1320
+ model = directives.get('model', 'gemini-2.0-flash')
1311
1321
  timeout = directives.get('timeout', 300)
1312
1322
  max_output_tokens = directives.get('max_output_tokens')
1313
1323
  temperature = directives.get('temperature', 0.0)
1314
- top_p = directives.get('top_p')
1324
+ top_p = directives.get('top_p', 1.0 if temperature == 0.0 else None)
1315
1325
  top_k = directives.get('top_k')
1316
-
1317
1326
  GOOGLE_AI_API_KEY = os.environ.get('GOOGLE_AI_API_KEY', '').rstrip()
1318
1327
  if len(GOOGLE_AI_API_KEY) != 39:
1319
1328
  logger.error(
@@ -1323,7 +1332,8 @@ class AIGoogleDiffer(DifferBase):
1323
1332
  return (
1324
1333
  f'## ERROR in summarizing changes using Google AI:\n'
1325
1334
  f'Environment variable GOOGLE_AI_API_KEY not found or is of the incorrect length '
1326
- f'{len(GOOGLE_AI_API_KEY)}.\n'
1335
+ f'{len(GOOGLE_AI_API_KEY)}.\n',
1336
+ '',
1327
1337
  )
1328
1338
 
1329
1339
  data: dict[str, Any] = {
@@ -1341,6 +1351,7 @@ class AIGoogleDiffer(DifferBase):
1341
1351
  if directives.get('tools'):
1342
1352
  data['tools'] = directives['tools']
1343
1353
  logger.info(f'Job {job.index_number}: Making the content generation request to Google AI model {model}')
1354
+ model_version = model # default
1344
1355
  try:
1345
1356
  r = httpx.Client(http2=True).post( # noqa: S113 Call to httpx without timeout
1346
1357
  f'https://generativelanguage.googleapis.com/v{api_version}/models/{model}:generateContent?'
@@ -1360,6 +1371,8 @@ class AIGoogleDiffer(DifferBase):
1360
1371
  f'AI summary unavailable: Model did not return any candidate output:\n'
1361
1372
  f'{jsonlib.dumps(result, ensure_ascii=True, indent=2)}'
1362
1373
  )
1374
+ model_version = result['modelVersion']
1375
+
1363
1376
  elif r.status_code == 400:
1364
1377
  summary = (
1365
1378
  f'AI summary unavailable: Received error from {r.url.host}: '
@@ -1377,7 +1390,7 @@ class AIGoogleDiffer(DifferBase):
1377
1390
  f'AI summary unavailable: HTTP client error: {e} when requesting data from ' f'{e.request.url.host}'
1378
1391
  )
1379
1392
 
1380
- return summary
1393
+ return summary, model_version
1381
1394
 
1382
1395
  def differ(
1383
1396
  self,
@@ -1394,8 +1407,8 @@ class AIGoogleDiffer(DifferBase):
1394
1407
  RuntimeWarning,
1395
1408
  )
1396
1409
 
1397
- def get_ai_summary(prompt: str, system_instructions: str) -> str:
1398
- """Generate AI summary from unified diff, or an error message"""
1410
+ def get_ai_summary(prompt: str, system_instructions: str) -> tuple[str, str]:
1411
+ """Generate AI summary from unified diff, or an error message, plus the model version."""
1399
1412
  GOOGLE_AI_API_KEY = os.environ.get('GOOGLE_AI_API_KEY', '').rstrip()
1400
1413
  if len(GOOGLE_AI_API_KEY) != 39:
1401
1414
  logger.error(
@@ -1405,12 +1418,10 @@ class AIGoogleDiffer(DifferBase):
1405
1418
  return (
1406
1419
  f'## ERROR in summarizing changes using {self.__kind__}:\n'
1407
1420
  f'Environment variable GOOGLE_AI_API_KEY not found or is of the incorrect length '
1408
- f'{len(GOOGLE_AI_API_KEY)}.\n'
1421
+ f'{len(GOOGLE_AI_API_KEY)}.\n',
1422
+ '',
1409
1423
  )
1410
1424
 
1411
- if 'model' not in directives:
1412
- directives['model'] = 'gemini-1.5-flash-latest' # also for footer
1413
-
1414
1425
  if '{unified_diff' in prompt: # matches unified_diff or unified_diff_new
1415
1426
  default_context_lines = 9999 if '{unified_diff}' in prompt else 0 # none if only unified_diff_new
1416
1427
  context_lines = directives.get('prompt_ud_context_lines', default_context_lines)
@@ -1427,7 +1438,7 @@ class AIGoogleDiffer(DifferBase):
1427
1438
  )
1428
1439
  if not unified_diff:
1429
1440
  # no changes
1430
- return ''
1441
+ return '', ''
1431
1442
  else:
1432
1443
  unified_diff = ''
1433
1444
 
@@ -1442,7 +1453,7 @@ class AIGoogleDiffer(DifferBase):
1442
1453
 
1443
1454
  # check if data is different (same data is sent during testing)
1444
1455
  if '{old_text}' in prompt and '{new_text}' in prompt and self.state.old_data == self.state.new_data:
1445
- return ''
1456
+ return '', ''
1446
1457
 
1447
1458
  model_prompt = prompt.format(
1448
1459
  unified_diff=unified_diff,
@@ -1451,14 +1462,14 @@ class AIGoogleDiffer(DifferBase):
1451
1462
  new_text=self.state.new_data,
1452
1463
  )
1453
1464
 
1454
- summary = self._send_to_model(
1465
+ summary, model_version = self._send_to_model(
1455
1466
  self.job,
1456
1467
  system_instructions,
1457
1468
  model_prompt,
1458
1469
  directives=directives,
1459
1470
  )
1460
1471
 
1461
- return summary
1472
+ return summary, model_version
1462
1473
 
1463
1474
  if directives.get('additions_only') or self.job.additions_only:
1464
1475
  default_system_instructions = (
@@ -1474,7 +1485,7 @@ class AIGoogleDiffer(DifferBase):
1474
1485
  'You are a skilled journalist tasked with analyzing two versions of a text and summarizing the key '
1475
1486
  'differences in meaning between them. The audience for your summary is already familiar with the '
1476
1487
  "text's content, so you can focus on the most significant changes.\n\n"
1477
- "**Instructions:**\n\n'"
1488
+ '**Instructions:**\n\n'
1478
1489
  '1. Carefully examine the old version of the text, provided within the `<old_version>` and '
1479
1490
  '`</old_version>` tags.\n'
1480
1491
  '2. Carefully examine the new version of the text, provided within the `<new_version>` and '
@@ -1485,25 +1496,38 @@ class AIGoogleDiffer(DifferBase):
1485
1496
  '5. Summarize the identified differences, except those ignored, in a clear and concise manner, '
1486
1497
  'explaining how the meaning has shifted or evolved in the new version compared to the old version only '
1487
1498
  'when necessary. Be specific and provide examples to illustrate your points when needed.\n'
1488
- '6. If ther are only additions to the text, then summarize the additions.\n'
1499
+ '6. If there are only additions to the text, then summarize the additions.\n'
1489
1500
  '7. Use Markdown formatting to structure your summary effectively. Use headings, bullet points, '
1490
1501
  'and other Markdown elements as needed to enhance readability.\n'
1491
1502
  '8. Restrict your analysis and summary to the information provided within the `<old_version>` and '
1492
- '`<new_version> tags. Do not introduce external information or assumptions.\n'
1503
+ '`<new_version>` tags. Do not introduce external information or assumptions.\n'
1493
1504
  )
1494
1505
  default_prompt = '<old_version>\n{old_text}\n</old_version>\n\n<new_version>\n{new_text}\n</new_version>'
1495
1506
  system_instructions = directives.get('system_instructions', default_system_instructions)
1496
1507
  prompt = directives.get('prompt', default_prompt).replace('\\n', '\n')
1497
- summary = get_ai_summary(prompt, system_instructions)
1508
+ summary, model_version = get_ai_summary(prompt, system_instructions)
1498
1509
  if not summary:
1499
1510
  self.state.verb = 'changed,no_report'
1500
1511
  return {'text': '', 'markdown': '', 'html': ''}
1501
1512
  newline = '\n' # For Python < 3.12 f-string {} compatibility
1502
1513
  back_n = '\\n' # For Python < 3.12 f-string {} compatibility
1503
- directives_text = (
1504
- ', '.join(f'{key}={str(value).replace(newline, back_n)}' for key, value in directives.items()) or 'None'
1514
+ directives.pop('model', None)
1515
+ if directives:
1516
+ directives_text = (
1517
+ ' (differ directive(s): '
1518
+ + (
1519
+ ', '.join(f'{key}={str(value).replace(newline, back_n)}' for key, value in directives.items())
1520
+ or 'None'
1521
+ )
1522
+ + ')'
1523
+ )
1524
+ else:
1525
+ directives_text = ''
1526
+ footer = (
1527
+ f"Summary by Google Generative AI's model {model_version}{directives_text}"
1528
+ if model_version and directives_text
1529
+ else ''
1505
1530
  )
1506
- footer = f'Summary generated by Google Generative AI (differ directive(s): {directives_text})'
1507
1531
  temp_unfiltered_diff: dict[Literal['text', 'markdown', 'html'], str] = {}
1508
1532
  for rep_kind in ['text', 'html']: # markdown is same as text
1509
1533
  unified_report = DifferBase.process(
@@ -1515,17 +1539,16 @@ class AIGoogleDiffer(DifferBase):
1515
1539
  temp_unfiltered_diff,
1516
1540
  )
1517
1541
  return {
1518
- 'text': f"{summary}\n\n{unified_report['text']}\n------------\n{footer}",
1519
- 'markdown': f"{summary}\n\n{unified_report['markdown']}\n* * *\n{footer}",
1542
+ 'text': f"{summary}\n\n{unified_report['text']}" + (f'\n------------\n{footer}' if footer else ''),
1543
+ 'markdown': f"{summary}\n\n{unified_report['markdown']}" + (f'\n* * *\n{footer}' if footer else ''),
1520
1544
  'html': '\n'.join(
1521
1545
  [
1522
1546
  mark_to_html(summary, extras={'tables'}).replace('<h2>', '<h3>').replace('</h2>', '</h3>'),
1523
1547
  '<br>',
1524
1548
  '<br>',
1525
1549
  unified_report['html'],
1526
- '-----<br>',
1527
- f'<i><small>{footer}</small></i>',
1528
1550
  ]
1551
+ + (['-----<br>', f'<i><small>{footer}</small></i>'] if footer else [])
1529
1552
  ),
1530
1553
  }
1531
1554
 
@@ -194,7 +194,7 @@ class FilterBase(metaclass=TrackSubClasses):
194
194
  if unknown_keys and '<any>' not in allowed_keys:
195
195
  raise ValueError(
196
196
  f'Job {job_index_number}: Filter {filter_kind} does not support subfilter or filter '
197
- f"directive(s) {', '.join(unknown_keys)} (supported: {', '.join(allowed_keys)})."
197
+ f"directive(s) {', '.join(unknown_keys)}. Only {', '.join(allowed_keys)} are supported."
198
198
  )
199
199
 
200
200
  yield filter_kind, subfilter
@@ -794,7 +794,10 @@ class FormatJsonFilter(FilterBase):
794
794
  self.job.set_to_monospace()
795
795
  sort_keys = subfilter.get('sort_keys', False)
796
796
  indentation = int(subfilter.get('indentation', 4))
797
- parsed_json = jsonlib.loads(data)
797
+ try:
798
+ parsed_json = jsonlib.loads(data)
799
+ except jsonlib.JSONDecodeError as e:
800
+ return f"Filter '{self.__kind__}' returned JSONDecodeError: {e}\n\n{data!s}", mime_type
798
801
  if not mime_type.endswith('json'):
799
802
  mime_type = 'application/json'
800
803
  return jsonlib.dumps(parsed_json, ensure_ascii=False, sort_keys=sort_keys, indent=indentation), mime_type
@@ -67,7 +67,7 @@ class JobState(ContextManager):
67
67
  exception: Exception | None = None
68
68
  generated_diff: dict[Literal['text', 'markdown', 'html'], str]
69
69
  history_dic_snapshots: dict[str | bytes, Snapshot]
70
- new_data: str | bytes
70
+ new_data: str | bytes = ''
71
71
  new_error_data: ErrorData = {}
72
72
  new_etag: str
73
73
  new_mime_type: str = ''
@@ -323,7 +323,7 @@ class JobState(ContextManager):
323
323
  _generated_diff, _mime_type = FilterBase.process( # type: ignore[assignment]
324
324
  filter_kind, subfilter, self, _generated_diff, _mime_type
325
325
  )
326
- self.generated_diff[report_kind] = _generated_diff
326
+ self.generated_diff[report_kind] = str(_generated_diff)
327
327
 
328
328
  return self.generated_diff[report_kind]
329
329
 
@@ -370,7 +370,7 @@ class Report:
370
370
 
371
371
  :param job_state: The JobState object with the information of the job run.
372
372
  """
373
- if job_state.exception is not None and job_state.exception is not NotModifiedError:
373
+ if job_state.exception is not None and not isinstance(job_state.exception, NotModifiedError):
374
374
  logger.info(
375
375
  f'Job {job_state.job.index_number}: Got exception while processing job {job_state.job}',
376
376
  exc_info=job_state.exception,
@@ -786,7 +786,6 @@ class UrlJob(UrlJobBase):
786
786
  http_error_msg = f'{response.status_code} Client Error: {reason} for url: {response.url}'
787
787
  else:
788
788
  http_error_msg = f'{response.status_code} Server Error: {reason} for url: {response.url}'
789
- logger.error(f'httpx received {http_error_msg}')
790
789
 
791
790
  if response.status_code != 404:
792
791
  try:
@@ -858,8 +857,7 @@ class UrlJob(UrlJobBase):
858
857
  urllib3.util.ssl_.DEFAULT_CIPHERS += 'HIGH:!DH:!aNULL' # type: ignore[attr-defined]
859
858
  except AttributeError:
860
859
  logger.error(
861
- 'Unable to ignore_dh_key_too_small due to bug in '
862
- 'requests.packages.urrlib3.util.ssl.DEFAULT_CIPHERS'
860
+ 'Unable to ignore_dh_key_too_small due to bug in requests.packages.urrlib3.util.ssl.DEFAULT_CIPHERS'
863
861
  )
864
862
  logger.error('See https://github.com/psf/requests/issues/6443')
865
863
 
@@ -1105,7 +1103,6 @@ class UrlJob(UrlJobBase):
1105
1103
  ):
1106
1104
  # Instead of a full traceback, just show the error
1107
1105
  exception_str = str(exception).strip()
1108
- print(f'{exception_str=} {exception.args=} {type(exception)=}')
1109
1106
  if self.proxy and (
1110
1107
  (httpx and isinstance(exception, httpx.TransportError))
1111
1108
  or any(
@@ -1373,7 +1370,7 @@ class BrowserJob(UrlJobBase):
1373
1370
  ignore_default_args=ignore_default_args,
1374
1371
  timeout=timeout,
1375
1372
  headless=headless,
1376
- proxy=proxy,
1373
+ proxy=proxy, # type: ignore[arg-type]
1377
1374
  )
1378
1375
  browser_version = browser.version
1379
1376
  user_agent = headers.pop(
@@ -1402,7 +1399,7 @@ class BrowserJob(UrlJobBase):
1402
1399
  args=args,
1403
1400
  ignore_default_args=ignore_default_args,
1404
1401
  headless=headless,
1405
- proxy=proxy,
1402
+ proxy=proxy, # type: ignore[arg-type]
1406
1403
  no_viewport=no_viewport,
1407
1404
  ignore_https_errors=self.ignore_https_errors,
1408
1405
  extra_http_headers=dict(headers),
@@ -1811,7 +1808,7 @@ class BrowserJob(UrlJobBase):
1811
1808
  :param tb: The traceback.format_exc() string.
1812
1809
  :returns: A string to display and/or use in reports.
1813
1810
  """
1814
- exception_str = f'Browser error in {str(exception).strip()}'
1811
+ exception_str = str(exception).strip()
1815
1812
  print(f'{exception_str=}, {tb=}')
1816
1813
  if self.proxy and 'net::ERR' in exception_str:
1817
1814
  exception_str += f'\n\n(Job has proxy {self.proxy})'
@@ -117,7 +117,7 @@ if sys.platform == 'win32':
117
117
  try:
118
118
  from colorama import AnsiToWin32
119
119
  except ImportError as e: # pragma: no cover
120
- AnsiToWin32 = str(e) # type: ignore[assignment]
120
+ AnsiToWin32 = str(e) # type: ignore[assignment,misc]
121
121
 
122
122
  logger = logging.getLogger(__name__)
123
123
 
@@ -750,8 +750,12 @@ class YamlConfigStorage(BaseYamlFileStorage):
750
750
  if 'job_defaults' in config_for_extras:
751
751
  # Create missing 'job_defaults' keys from DEFAULT_CONFIG
752
752
  for key in DEFAULT_CONFIG['job_defaults']:
753
+ if 'job_defaults' not in config_for_extras:
754
+ config_for_extras['job_defaults'] = {}
753
755
  config_for_extras['job_defaults'][key] = None # type: ignore[literal-required]
754
756
  for key in DEFAULT_CONFIG['differ_defaults']:
757
+ if 'differ_defaults' not in config_for_extras:
758
+ config_for_extras['differ_defaults'] = {}
755
759
  config_for_extras['differ_defaults'][key] = None # type: ignore[literal-required]
756
760
  if 'hooks' in sys.modules:
757
761
  # Remove extra keys in config used in hooks (they are not in DEFAULT_CONFIG)
@@ -794,8 +798,8 @@ class YamlConfigStorage(BaseYamlFileStorage):
794
798
  )
795
799
  else:
796
800
  config['job_defaults']['command'] = config['job_defaults'].pop(
797
- 'shell'
798
- ) # type: ignore[typeddict-item]
801
+ 'shell' # type: ignore[typeddict-item]
802
+ )
799
803
 
800
804
  for key in {'all', 'url', 'browser', 'command'}:
801
805
  if key not in config['job_defaults']:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: webchanges
3
- Version: 3.28.0rc0
3
+ Version: 3.28.2
4
4
  Summary: Web Changes Delivered. AI-Summarized. Totally Anonymous.
5
5
  Author-email: Mike Borsetti <mike+webchanges@borsetti.com>
6
6
  Maintainer-email: Mike Borsetti <mike+webchanges@borsetti.com>
@@ -195,7 +195,9 @@ For Generative AI summaries (BETA), you need a free `API Key from Google Cloud A
195
195
 
196
196
  Installation
197
197
  ============
198
- Install **webchanges** |pypi_version| |format| |status| |security| with:
198
+ |pypi_version| |format| |status| |security|
199
+
200
+ Install **webchanges** with:
199
201
 
200
202
  .. code-block:: bash
201
203
 
@@ -203,7 +205,7 @@ Install **webchanges** |pypi_version| |format| |status| |security| with:
203
205
 
204
206
  Running in Docker
205
207
  -----------------
206
- **webchanges** can easily run in a container; you can find a `Docker <https://www.docker.com/>`__ implementation
208
+ **webchanges** can easily run in a container and you will find a `Docker <https://www.docker.com/>`__ implementation
207
209
  `here <https://github.com/yubiuser/webchanges-docker>`__.
208
210
 
209
211
 
@@ -247,20 +249,19 @@ Schedule
247
249
  --------
248
250
  **webchanges** leverages the power of a system scheduler:
249
251
 
250
- - On Linux you can use cron, and a tool like `crontab.guru <https://crontab.guru>`__ can build a
251
- schedule expression for you (note: see `here <https://www.computerhope.com/unix/ucrontab.htm>`__ if you have never
252
- used cron before);
252
+ - On Linux you can use cron, with the help of a tool like `crontab.guru <https://crontab.guru>`__ (help `here
253
+ <https://www.computerhope.com/unix/ucrontab.htm>`__);
253
254
  - On Windows you can use `Windows Task Scheduler <https://en.wikipedia.org/wiki/Windows_Task_Scheduler>`__;
254
255
  - On macOS you can use `launchd <https://developer.apple
255
- .com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html>`__ (note: see `here
256
- <https://launchd.info/>`__ if you have never used launchd before).
256
+ .com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html>`__ (help `here
257
+ <https://launchd.info/>`__).
257
258
 
258
259
 
259
260
  Code
260
261
  ====
261
- |coveralls| |issues|
262
+ |coveralls| |issues| |code_style|
262
263
 
263
- The code and issues tracker are hosted on `GitHub <https://github.com/mborsetti/webchanges>`__.
264
+ The code, issues tracker, and discussions are hosted on `GitHub <https://github.com/mborsetti/webchanges>`__.
264
265
 
265
266
 
266
267
  Contributing
@@ -344,7 +345,7 @@ Example enhancements to HTML reporting:
344
345
  .. |format| image:: https://img.shields.io/pypi/format/webchanges.svg
345
346
  :target: https://pypi.org/project/webchanges/
346
347
  :alt: Kit format
347
- .. |downloads| image:: https://static.pepy.tech/badge/webchanges
348
+ .. |downloads| image:: https://img.shields.io/pypi/dm/webchanges.svg
348
349
  :target: https://www.pepy.tech/project/webchanges
349
350
  :alt: PyPI downloads
350
351
  .. |license| image:: https://img.shields.io/pypi/l/webchanges.svg
@@ -356,15 +357,24 @@ Example enhancements to HTML reporting:
356
357
  .. |readthedocs| image:: https://img.shields.io/readthedocs/webchanges/stable.svg?label=
357
358
  :target: https://webchanges.readthedocs.io/
358
359
  :alt: Documentation status
359
- .. |CI| image:: https://github.com/mborsetti/webchanges/actions/workflows/ci-cd.yaml/badge.svg?event=push
360
+ .. |old_CI| image:: https://github.com/mborsetti/webchanges/actions/workflows/ci-cd.yaml/badge.svg?event=push
361
+ :target: https://github.com/mborsetti/webchanges/actions
362
+ :alt: CI testing status
363
+ .. |CI| image:: https://img.shields.io/github/check-runs/mborsetti/webchanges/main
360
364
  :target: https://github.com/mborsetti/webchanges/actions
361
365
  :alt: CI testing status
362
- .. |coveralls| image:: https://coveralls.io/repos/github/mborsetti/webchanges/badge.svg?branch=main
366
+ .. |old_coveralls| image:: https://coveralls.io/repos/github/mborsetti/webchanges/badge.svg?branch=main
367
+ :target: https://coveralls.io/github/mborsetti/webchanges?branch=main
368
+ :alt: Code coverage by Coveralls
369
+ .. |coveralls| image:: https://img.shields.io/coverallsCoverage/github/mborsetti/webchanges.svg
363
370
  :target: https://coveralls.io/github/mborsetti/webchanges?branch=main
364
371
  :alt: Code coverage by Coveralls
372
+ .. |code_style| image:: https://img.shields.io/badge/code%20style-black-000000.svg
373
+ :target: https://github.com/psf/black
374
+ :alt: Code style black
365
375
  .. |status| image:: https://img.shields.io/pypi/status/webchanges.svg
366
376
  :target: https://pypi.org/project/webchanges/
367
377
  :alt: Package stability
368
- .. |security| image:: https://img.shields.io/badge/security-bandit-yellow.svg
378
+ .. |security| image:: https://img.shields.io/badge/security-bandit-green.svg
369
379
  :target: https://github.com/PyCQA/bandit
370
380
  :alt: Security Status
@@ -4,6 +4,7 @@ README.rst
4
4
  pyproject.toml
5
5
  requirements.txt
6
6
  webchanges/__init__.py
7
+ webchanges/__main__.py
7
8
  webchanges/cli.py
8
9
  webchanges/command.py
9
10
  webchanges/config.py
File without changes
File without changes
File without changes