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.
- {webchanges-3.28.0rc0/webchanges.egg-info → webchanges-3.28.2}/PKG-INFO +24 -14
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/README.rst +23 -13
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/__init__.py +1 -1
- webchanges-3.28.2/webchanges/__main__.py +10 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/cli.py +11 -7
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/command.py +2 -2
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/differs.py +76 -53
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/filters.py +5 -2
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/handler.py +3 -3
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/jobs.py +4 -7
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/reporters.py +1 -1
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/storage.py +6 -2
- {webchanges-3.28.0rc0 → webchanges-3.28.2/webchanges.egg-info}/PKG-INFO +24 -14
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges.egg-info/SOURCES.txt +1 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/LICENSE +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/MANIFEST.in +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/pyproject.toml +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/requirements.txt +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/setup.cfg +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/_vendored/__init__.py +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/_vendored/headers.py +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/_vendored/packaging_version.py +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/config.py +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/mailer.py +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/main.py +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/py.typed +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/storage_minidb.py +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/util.py +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges/worker.py +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges.egg-info/dependency_links.txt +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges.egg-info/entry_points.txt +0 -0
- {webchanges-3.28.0rc0 → webchanges-3.28.2}/webchanges.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
-
|
|
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
|
|
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,
|
|
251
|
-
|
|
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>`__ (
|
|
256
|
-
<https://launchd.info/>`__
|
|
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
|
|
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://
|
|
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
|
-
.. |
|
|
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
|
-
.. |
|
|
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-
|
|
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
|
-
|
|
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
|
|
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,
|
|
85
|
-
|
|
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>`__ (
|
|
90
|
-
<https://launchd.info/>`__
|
|
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
|
|
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://
|
|
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
|
-
.. |
|
|
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
|
-
.. |
|
|
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-
|
|
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.
|
|
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'
|
|
@@ -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
|
-
|
|
248
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
502
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1112
|
-
f"{additional_parts[
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
'
|
|
1116
|
-
'
|
|
1117
|
-
'
|
|
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':
|
|
1250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1504
|
-
|
|
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)}
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
)
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,
|
|
251
|
-
|
|
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>`__ (
|
|
256
|
-
<https://launchd.info/>`__
|
|
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
|
|
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://
|
|
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
|
-
.. |
|
|
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
|
-
.. |
|
|
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-
|
|
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
|
|
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
|