webchanges 3.38.0__tar.gz → 3.39.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {webchanges-3.38.0/webchanges.egg-info → webchanges-3.39.0}/PKG-INFO +21 -20
- {webchanges-3.38.0 → webchanges-3.39.0}/README.rst +21 -20
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/__init__.py +1 -1
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/_resources/config.schema.json +20 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/_resources/jobs.schema.json +27 -3
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/cli.py +5 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/command.py +98 -18
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/config.py +21 -9
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/filters/_html.py +11 -6
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/jobs/_base.py +50 -3
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/jobs/_shell.py +43 -5
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/jobs/_url.py +10 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/mailer.py +10 -1
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/reporters/__init__.py +3 -2
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/reporters/_base.py +3 -2
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/reporters/_email.py +5 -2
- webchanges-3.39.0/webchanges/reporters/_system.py +132 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/reporters/_web.py +79 -47
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/storage/__init__.py +2 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/storage/_config.py +15 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/storage/_yaml.py +52 -7
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/worker.py +8 -1
- {webchanges-3.38.0 → webchanges-3.39.0/webchanges.egg-info}/PKG-INFO +21 -20
- webchanges-3.38.0/webchanges/reporters/_system.py +0 -71
- {webchanges-3.38.0 → webchanges-3.39.0}/LICENSE.md +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/MANIFEST.in +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/pyproject.toml +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/requirements.txt +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/setup.cfg +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/setup.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/tools/get_manpages_parser.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/__main__.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/_resources/__init__.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/_vendored/__init__.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/_vendored/headers.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/_vendored/packaging_version.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/_vendored/typeguard.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/differs.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/filters/__init__.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/filters/_base.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/filters/_document.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/filters/_encoding.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/filters/_shell.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/filters/_text.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/handler.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/jobs/__init__.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/jobs/_browser.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/jobs/_exceptions.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/main.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/py.typed +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/reporters/_browser.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/reporters/_chat.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/reporters/_stdout.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/storage/_base.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/storage/_minidb.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/storage/_redis.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/storage/_sqlite3.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/storage/_ssdb.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/storage_minidb.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges/util.py +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges.egg-info/SOURCES.txt +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges.egg-info/dependency_links.txt +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges.egg-info/entry_points.txt +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges.egg-info/requires.txt +0 -0
- {webchanges-3.38.0 → webchanges-3.39.0}/webchanges.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webchanges
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.39.0
|
|
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>
|
|
@@ -123,22 +123,25 @@ notifications and AI-powered summaries to your favorite `platform
|
|
|
123
123
|
|
|
124
124
|
Highlights
|
|
125
125
|
==========
|
|
126
|
-
- **
|
|
127
|
-
<https://webchanges.readthedocs.io/en/stable/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
- **
|
|
131
|
-
<https://webchanges.readthedocs.io/en/stable/differs.html#
|
|
132
|
-
- **
|
|
133
|
-
<https://webchanges.readthedocs.io/en/stable/
|
|
134
|
-
- **
|
|
126
|
+
- **Watch anything**: web pages (in a `real browser
|
|
127
|
+
<https://webchanges.readthedocs.io/en/stable/jobs.html#use-browser>`__ if needed), APIs, local files and command
|
|
128
|
+
output.
|
|
129
|
+
- **Track only what matters** with over 30 `filters <https://webchanges.readthedocs.io/en/stable/filters.html>`__.
|
|
130
|
+
- **Read changes at a glance** in formatted HTML, side by side, word by word, as `AI summaries
|
|
131
|
+
<https://webchanges.readthedocs.io/en/stable/differs.html#ai-google-diff>`__ and more.
|
|
132
|
+
- **Get notified anywhere**: email, Telegram, Discord, ntfy, webhooks and `more
|
|
133
|
+
<https://webchanges.readthedocs.io/en/stable/reporters.html>`__.
|
|
134
|
+
- **Few false alarms**: transient errors can be held back and repeated ones reported `once
|
|
135
|
+
<https://webchanges.readthedocs.io/en/stable/jobs.html#suppress-repeated-errors>`__.
|
|
136
|
+
- **Runs on your machine**, total privacy, no account needed, and extends with Python `hooks
|
|
137
|
+
<https://webchanges.readthedocs.io/en/stable/hooks.html>`__.
|
|
135
138
|
|
|
136
139
|
|
|
137
140
|
Requirements
|
|
138
141
|
============
|
|
139
142
|
**webchanges** requires |support|. For the best experience, use the current version of `Python
|
|
140
|
-
<https://www.python.org/downloads/>`__; older Python versions are supported for 3 years after they're replaced.
|
|
141
|
-
|
|
143
|
+
<https://www.python.org/downloads/>`__; older Python versions are supported for 3 years after they're replaced.
|
|
144
|
+
Free-threaded Python is supported.
|
|
142
145
|
|
|
143
146
|
For Generative AI summaries (BETA), you need a free `API Key from Google Cloud AI Studio
|
|
144
147
|
<https://aistudio.google.com/app/apikey>`__ (see `here
|
|
@@ -224,7 +227,7 @@ Schedule
|
|
|
224
227
|
|
|
225
228
|
Code
|
|
226
229
|
====
|
|
227
|
-
|coveralls| |issues| |code_style|
|
|
230
|
+
|coveralls| |issues| |code_style|
|
|
228
231
|
|
|
229
232
|
The code, issues tracker, and discussions are hosted on `GitHub <https://github.com/mborsetti/webchanges>`__.
|
|
230
233
|
|
|
@@ -243,10 +246,11 @@ License
|
|
|
243
246
|
=======
|
|
244
247
|
|license|
|
|
245
248
|
|
|
246
|
-
See the `complete licenses
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
licensed under a
|
|
249
|
+
See the `complete licenses
|
|
250
|
+
<https://raw.githubusercontent.com/mborsetti/webchanges/refs/heads/main/LICENSE.md>`__ (released under the `MIT
|
|
251
|
+
License <https://opensource.org/licenses/MIT>`__ but redistributing modified source code, dated 30 July 2020, from
|
|
252
|
+
`urlwatch 2.21 <https://github.com/thp/urlwatch/tree/346b25914b0418342ffe2fb0529bed702fddc01f>`__ licensed under a
|
|
253
|
+
`BSD 3-Clause License
|
|
250
254
|
<https://raw.githubusercontent.com/thp/urlwatch/346b25914b0418342ffe2fb0529bed702fddc01f/COPYING>`__).
|
|
251
255
|
|
|
252
256
|
|
|
@@ -302,6 +306,3 @@ see the `comparison <https://webchanges.readthedocs.io/en/stable/upgrading.html>
|
|
|
302
306
|
.. .. |snyk| image:: https://snyk.io/advisor/python/webchanges/badge.svg
|
|
303
307
|
.. :target: https://security.snyk.io/package/pip/webchanges
|
|
304
308
|
.. :alt: Snyk Package Health Score
|
|
305
|
-
.. |OpenSSF_Scorecard| image:: https://api.scorecard.dev/projects/github.com/mborsetti/webchanges/badge
|
|
306
|
-
:target: https://scorecard.dev/viewer/?uri=github.com/mborsetti/webchanges
|
|
307
|
-
:alt: OpenSSF Scoreard
|
|
@@ -16,22 +16,25 @@ notifications and AI-powered summaries to your favorite `platform
|
|
|
16
16
|
|
|
17
17
|
Highlights
|
|
18
18
|
==========
|
|
19
|
-
- **
|
|
20
|
-
<https://webchanges.readthedocs.io/en/stable/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- **
|
|
24
|
-
<https://webchanges.readthedocs.io/en/stable/differs.html#
|
|
25
|
-
- **
|
|
26
|
-
<https://webchanges.readthedocs.io/en/stable/
|
|
27
|
-
- **
|
|
19
|
+
- **Watch anything**: web pages (in a `real browser
|
|
20
|
+
<https://webchanges.readthedocs.io/en/stable/jobs.html#use-browser>`__ if needed), APIs, local files and command
|
|
21
|
+
output.
|
|
22
|
+
- **Track only what matters** with over 30 `filters <https://webchanges.readthedocs.io/en/stable/filters.html>`__.
|
|
23
|
+
- **Read changes at a glance** in formatted HTML, side by side, word by word, as `AI summaries
|
|
24
|
+
<https://webchanges.readthedocs.io/en/stable/differs.html#ai-google-diff>`__ and more.
|
|
25
|
+
- **Get notified anywhere**: email, Telegram, Discord, ntfy, webhooks and `more
|
|
26
|
+
<https://webchanges.readthedocs.io/en/stable/reporters.html>`__.
|
|
27
|
+
- **Few false alarms**: transient errors can be held back and repeated ones reported `once
|
|
28
|
+
<https://webchanges.readthedocs.io/en/stable/jobs.html#suppress-repeated-errors>`__.
|
|
29
|
+
- **Runs on your machine**, total privacy, no account needed, and extends with Python `hooks
|
|
30
|
+
<https://webchanges.readthedocs.io/en/stable/hooks.html>`__.
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
Requirements
|
|
31
34
|
============
|
|
32
35
|
**webchanges** requires |support|. For the best experience, use the current version of `Python
|
|
33
|
-
<https://www.python.org/downloads/>`__; older Python versions are supported for 3 years after they're replaced.
|
|
34
|
-
|
|
36
|
+
<https://www.python.org/downloads/>`__; older Python versions are supported for 3 years after they're replaced.
|
|
37
|
+
Free-threaded Python is supported.
|
|
35
38
|
|
|
36
39
|
For Generative AI summaries (BETA), you need a free `API Key from Google Cloud AI Studio
|
|
37
40
|
<https://aistudio.google.com/app/apikey>`__ (see `here
|
|
@@ -117,7 +120,7 @@ Schedule
|
|
|
117
120
|
|
|
118
121
|
Code
|
|
119
122
|
====
|
|
120
|
-
|coveralls| |issues| |code_style|
|
|
123
|
+
|coveralls| |issues| |code_style|
|
|
121
124
|
|
|
122
125
|
The code, issues tracker, and discussions are hosted on `GitHub <https://github.com/mborsetti/webchanges>`__.
|
|
123
126
|
|
|
@@ -136,10 +139,11 @@ License
|
|
|
136
139
|
=======
|
|
137
140
|
|license|
|
|
138
141
|
|
|
139
|
-
See the `complete licenses
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
licensed under a
|
|
142
|
+
See the `complete licenses
|
|
143
|
+
<https://raw.githubusercontent.com/mborsetti/webchanges/refs/heads/main/LICENSE.md>`__ (released under the `MIT
|
|
144
|
+
License <https://opensource.org/licenses/MIT>`__ but redistributing modified source code, dated 30 July 2020, from
|
|
145
|
+
`urlwatch 2.21 <https://github.com/thp/urlwatch/tree/346b25914b0418342ffe2fb0529bed702fddc01f>`__ licensed under a
|
|
146
|
+
`BSD 3-Clause License
|
|
143
147
|
<https://raw.githubusercontent.com/thp/urlwatch/346b25914b0418342ffe2fb0529bed702fddc01f/COPYING>`__).
|
|
144
148
|
|
|
145
149
|
|
|
@@ -194,7 +198,4 @@ see the `comparison <https://webchanges.readthedocs.io/en/stable/upgrading.html>
|
|
|
194
198
|
.. :alt: Security Status
|
|
195
199
|
.. .. |snyk| image:: https://snyk.io/advisor/python/webchanges/badge.svg
|
|
196
200
|
.. :target: https://security.snyk.io/package/pip/webchanges
|
|
197
|
-
.. :alt: Snyk Package Health Score
|
|
198
|
-
.. |OpenSSF_Scorecard| image:: https://api.scorecard.dev/projects/github.com/mborsetti/webchanges/badge
|
|
199
|
-
:target: https://scorecard.dev/viewer/?uri=github.com/mborsetti/webchanges
|
|
200
|
-
:alt: OpenSSF Scoreard
|
|
201
|
+
.. :alt: Snyk Package Health Score
|
|
@@ -22,7 +22,7 @@ __project_name__ = str(__package__)
|
|
|
22
22
|
# * MINOR version when you add functionality in a backwards compatible manner, and
|
|
23
23
|
# * MICRO or PATCH version when you make backwards compatible bug fixes. We no longer use '0'
|
|
24
24
|
# If unsure on increments, use pkg_resources.parse_version to parse
|
|
25
|
-
__version__ = '3.
|
|
25
|
+
__version__ = '3.39.0'
|
|
26
26
|
__description__ = (
|
|
27
27
|
'Check web (or command output) for changes since last run and notify.\n\nAnonymously alerts you of web changes.'
|
|
28
28
|
)
|
|
@@ -334,6 +334,11 @@
|
|
|
334
334
|
"description": "Sender's email address. Do not use a primary mailbox; create a throwaway account for webchanges.",
|
|
335
335
|
"default": ""
|
|
336
336
|
},
|
|
337
|
+
"reply_to": {
|
|
338
|
+
"type": "string",
|
|
339
|
+
"description": "Reply-To email address(es), used by email clients when replying to the report. For multiple addresses, concatenate with commas. If blank, no Reply-To header is sent.",
|
|
340
|
+
"default": ""
|
|
341
|
+
},
|
|
337
342
|
"subject": {
|
|
338
343
|
"type": "string",
|
|
339
344
|
"description": "Subject line. Use {count}, {jobs}, and {jobs_files} placeholders (see html.title for details).",
|
|
@@ -984,6 +989,21 @@
|
|
|
984
989
|
{ "$ref": "#/$defs/_ConfigReportXmpp" },
|
|
985
990
|
{ "type": "array", "items": { "$ref": "#/$defs/_ConfigReportXmpp" }, "minItems": 1 }
|
|
986
991
|
]
|
|
992
|
+
},
|
|
993
|
+
"mattermost": {
|
|
994
|
+
"type": ["object", "array"],
|
|
995
|
+
"description": "Deprecated (legacy); replace with 'webhook' and add 'markdown: true'. Still runs as a webhook reporter with Markdown.",
|
|
996
|
+
"deprecated": true
|
|
997
|
+
},
|
|
998
|
+
"shell": {
|
|
999
|
+
"type": ["object", "array"],
|
|
1000
|
+
"description": "Deprecated (legacy); replace with 'run_command'. Still runs its 'command' (a string or a list of arguments, without a shell) with the text report on its standard input; sub-directives 'command', 'ignore_stdout' (default true) and 'ignore_stderr' (default false).",
|
|
1001
|
+
"deprecated": true
|
|
1002
|
+
},
|
|
1003
|
+
"slack": {
|
|
1004
|
+
"type": ["object", "array"],
|
|
1005
|
+
"description": "Deprecated (legacy); rename to 'webhook' (same sub-directives). Still runs as a webhook reporter.",
|
|
1006
|
+
"deprecated": true
|
|
987
1007
|
}
|
|
988
1008
|
}
|
|
989
1009
|
},
|
|
@@ -169,6 +169,11 @@
|
|
|
169
169
|
"description": "If true, accepts a server's weak (1024-bit) Diffie-Hellman key by lowering the OpenSSL security level (certificates are still verified). Supported by http_client httpx and requests, not by curl_cffi.",
|
|
170
170
|
"default": false
|
|
171
171
|
},
|
|
172
|
+
"ignore_incomplete_reads": {
|
|
173
|
+
"type": "boolean",
|
|
174
|
+
"description": "If true, suppresses errors caused by the server closing the connection before sending the complete response. Not supported with use_browser: true.",
|
|
175
|
+
"default": false
|
|
176
|
+
},
|
|
172
177
|
"params": {
|
|
173
178
|
"type": "object",
|
|
174
179
|
"additionalProperties": { "type": "string" },
|
|
@@ -369,9 +374,22 @@
|
|
|
369
374
|
"deprecated": true,
|
|
370
375
|
"deprecationMessage": "Use 'url' with 'use_browser: true' instead of 'navigate'."
|
|
371
376
|
},
|
|
377
|
+
"useragent": {
|
|
378
|
+
"type": "string",
|
|
379
|
+
"description": "Deprecated (legacy): set a 'User-Agent' header in 'headers' instead.",
|
|
380
|
+
"deprecated": true,
|
|
381
|
+
"deprecationMessage": "Set a 'User-Agent' header in 'headers' instead of 'useragent'."
|
|
382
|
+
},
|
|
383
|
+
"browser": {
|
|
384
|
+
"type": "string",
|
|
385
|
+
"enum": ["chromium", "firefox", "webkit"],
|
|
386
|
+
"description": "Deprecated (legacy): use 'use_browser: <browser>' instead ('chromium' is 'use_browser: true').",
|
|
387
|
+
"deprecated": true,
|
|
388
|
+
"deprecationMessage": "Use 'use_browser: <browser>' instead of 'browser'."
|
|
389
|
+
},
|
|
372
390
|
"wait_for": {
|
|
373
391
|
"type": ["integer", "string"],
|
|
374
|
-
"description": "No longer supported (Pyppeteer): use 'wait_for_function', 'wait_for_selector' or 'wait_for_timeout' instead.",
|
|
392
|
+
"description": "No longer supported (Pyppeteer): use 'wait_for_function', 'wait_for_selector' or 'wait_for_timeout' instead. In legacy 'navigate' jobs, a selector, run as 'wait_for_selector'.",
|
|
375
393
|
"deprecated": true,
|
|
376
394
|
"deprecationMessage": "Use 'wait_for_function', 'wait_for_selector' or 'wait_for_timeout' instead of 'wait_for'."
|
|
377
395
|
},
|
|
@@ -383,8 +401,9 @@
|
|
|
383
401
|
},
|
|
384
402
|
"stderr": {
|
|
385
403
|
"type": "string",
|
|
386
|
-
"
|
|
387
|
-
"
|
|
404
|
+
"enum": ["ignore", "forward", "fail", "stdout"],
|
|
405
|
+
"description": "What to do with the standard error output of a command job: 'ignore' reports it only when the command fails (exits with a non-zero code), 'forward' also writes it to webchanges' own standard error, 'fail' fails the job if there is any, and 'stdout' merges it into the standard output, i.e. into the data. For backward compatibility, 'urlwatch' is accepted for 'forward'.",
|
|
406
|
+
"default": "ignore"
|
|
388
407
|
},
|
|
389
408
|
"differ": {
|
|
390
409
|
"oneOf": [
|
|
@@ -744,6 +763,11 @@
|
|
|
744
763
|
"type": "boolean",
|
|
745
764
|
"description": "If true, only report the first occurrence of an error."
|
|
746
765
|
},
|
|
766
|
+
"treat_new_as_changed": {
|
|
767
|
+
"type": "boolean",
|
|
768
|
+
"description": "If true, report a new job (its first successful run) as changed, with its content shown as a diff from empty content, instead of as new.",
|
|
769
|
+
"default": false
|
|
770
|
+
},
|
|
747
771
|
"compared_versions": {
|
|
748
772
|
"type": "integer",
|
|
749
773
|
"description": "Number of versions to keep/compare."
|
|
@@ -72,7 +72,12 @@ def migrate_from_legacy(
|
|
|
72
72
|
('hooks.py', hooks_file),
|
|
73
73
|
]
|
|
74
74
|
if any(dest is not None for _, dest in yaml_dests):
|
|
75
|
+
# Like urlwatch itself: ~/.urlwatch if it exists, otherwise the platform's configuration directory (e.g.
|
|
76
|
+
# ~/.config/urlwatch on Linux).
|
|
75
77
|
legacy_project_path = Path.home() / f'.{legacy_package}'
|
|
78
|
+
if not legacy_project_path.is_dir():
|
|
79
|
+
legacy_project_path = platformdirs.user_config_path(legacy_package)
|
|
80
|
+
logger.debug('Looking for legacy %s files in %s', legacy_package, legacy_project_path)
|
|
76
81
|
if legacy_project_path.is_dir():
|
|
77
82
|
pairs.extend((legacy_project_path / name, dest) for name, dest in yaml_dests if dest is not None)
|
|
78
83
|
|
|
@@ -103,19 +103,23 @@ class UrlwatchCommand:
|
|
|
103
103
|
def jobs_from_joblist(self) -> Iterator[JobBase]:
|
|
104
104
|
"""Generates the jobs to process from the joblist and/or the tags entered in the CLI."""
|
|
105
105
|
if self.urlwatcher.urlwatch_config.joblist:
|
|
106
|
-
|
|
106
|
+
# A job selected more than once (e.g. by its index and by its URL, or by --prepare-jobs adding a new job
|
|
107
|
+
# that was also selected) is run only once.
|
|
108
|
+
jobs = list(
|
|
109
|
+
dict.fromkeys(self._find_job(job_entry) for job_entry in self.urlwatcher.urlwatch_config.joblist)
|
|
110
|
+
)
|
|
107
111
|
enabled_jobs = [job for job in jobs if job.is_enabled()]
|
|
108
|
-
disabled = len(
|
|
112
|
+
disabled = len(jobs) - len(enabled_jobs)
|
|
109
113
|
disabled_str = f' (excluding {disabled} disabled)' if disabled else ''
|
|
110
114
|
logger.debug(
|
|
111
|
-
f'Processing {len(enabled_jobs)} job{"s" if enabled_jobs else ""}{disabled_str} as
|
|
112
|
-
f'command line: {", ".join(str(j) for j in self.urlwatcher.urlwatch_config.joblist)}'
|
|
115
|
+
f'Processing {len(enabled_jobs)} job{"s" if len(enabled_jobs) != 1 else ""}{disabled_str} as '
|
|
116
|
+
f'specified in command line: {", ".join(str(j) for j in self.urlwatcher.urlwatch_config.joblist)}'
|
|
113
117
|
)
|
|
114
118
|
else:
|
|
115
119
|
enabled_jobs = [job for job in self.urlwatcher.jobs if job.is_enabled()]
|
|
116
|
-
disabled = len(
|
|
120
|
+
disabled = len(self.urlwatcher.jobs) - len(enabled_jobs)
|
|
117
121
|
disabled_str = f' (excluding {disabled} disabled)' if disabled else ''
|
|
118
|
-
logger.debug(f'Processing {len(enabled_jobs)} job{"s" if enabled_jobs else ""}{disabled_str}')
|
|
122
|
+
logger.debug(f'Processing {len(enabled_jobs)} job{"s" if len(enabled_jobs) != 1 else ""}{disabled_str}')
|
|
119
123
|
|
|
120
124
|
tags = self.tags_from_cli()
|
|
121
125
|
if tags:
|
|
@@ -196,11 +200,12 @@ class UrlwatchCommand:
|
|
|
196
200
|
def _find_job(self, query: str | int) -> JobBase:
|
|
197
201
|
"""Finds the job based on a query.
|
|
198
202
|
|
|
199
|
-
It is matched to the job index (also negative) or a job location (i.e. the url/user_visible_url
|
|
203
|
+
It is matched to the job index (also negative), a job name, or a job location (i.e. the url/user_visible_url
|
|
204
|
+
or command).
|
|
200
205
|
|
|
201
206
|
:param query: The query.
|
|
202
207
|
:return: The matching JobBase.
|
|
203
|
-
:raises
|
|
208
|
+
:raises ValueError: If no job, or more than one job, matches.
|
|
204
209
|
"""
|
|
205
210
|
if isinstance(query, int):
|
|
206
211
|
index = query
|
|
@@ -208,11 +213,25 @@ class UrlwatchCommand:
|
|
|
208
213
|
try:
|
|
209
214
|
index = int(query)
|
|
210
215
|
except ValueError:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
location = unquote_plus(query)
|
|
217
|
+
jobs = [
|
|
218
|
+
job
|
|
219
|
+
for job in self.urlwatcher.jobs
|
|
220
|
+
if job.name == query or unquote_plus(job.get_location()) == location
|
|
221
|
+
]
|
|
222
|
+
if not jobs:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"Job {query} does not match any job's name, url/user_visible_url or command."
|
|
225
|
+
) from None
|
|
226
|
+
if len(jobs) > 1:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f'Job {query} matches {len(jobs)} jobs; select the job by its index number instead:\n'
|
|
229
|
+
+ '\n'.join(
|
|
230
|
+
f'• {job.get_indexed_location()}' + (f' ({job.name})' if job.name else '') for job in jobs
|
|
231
|
+
)
|
|
232
|
+
) from None
|
|
233
|
+
logger.debug('Job %s matched %s', query, jobs[0].get_indexed_location())
|
|
234
|
+
return jobs[0]
|
|
216
235
|
|
|
217
236
|
if index == 0:
|
|
218
237
|
raise ValueError(f'Job index {index} out of range.')
|
|
@@ -226,7 +245,7 @@ class UrlwatchCommand:
|
|
|
226
245
|
def _find_job_with_defaults(self, query: str | int) -> JobBase:
|
|
227
246
|
"""Returns the job with defaults based on job_id.
|
|
228
247
|
|
|
229
|
-
This could match an index or a location (url/user_visible_url or command). Accepts negative numbers.
|
|
248
|
+
This could match an index, a name or a location (url/user_visible_url or command). Accepts negative numbers.
|
|
230
249
|
|
|
231
250
|
:param query: The query.
|
|
232
251
|
:return: The matching JobBase with defaults.
|
|
@@ -813,6 +832,8 @@ class UrlwatchCommand:
|
|
|
813
832
|
('--delete', self.urlwatch_config.delete),
|
|
814
833
|
('--add', self.urlwatch_config.add),
|
|
815
834
|
('--change-location', self.urlwatch_config.change_location),
|
|
835
|
+
('--enable', self.urlwatch_config.enable),
|
|
836
|
+
('--disable', self.urlwatch_config.disable),
|
|
816
837
|
)
|
|
817
838
|
if value is not None
|
|
818
839
|
)
|
|
@@ -829,10 +850,13 @@ class UrlwatchCommand:
|
|
|
829
850
|
if sys.__stdin__ and sys.__stdin__.isatty():
|
|
830
851
|
print(
|
|
831
852
|
f'WARNING: About to permanently delete {job.get_indexed_location()}.\n'
|
|
832
|
-
' Job file will be overwritten and all remarks lost
|
|
853
|
+
' Job file will be overwritten and all remarks lost.\n'
|
|
833
854
|
' This operation cannot be undone!\n'
|
|
834
855
|
)
|
|
835
|
-
|
|
856
|
+
try:
|
|
857
|
+
resp = input(" Please enter 'Y' to proceed: ")
|
|
858
|
+
except EOFError: # e.g. stdin redirected from NUL on Windows, which isatty() reports as a terminal
|
|
859
|
+
resp = ''
|
|
836
860
|
if not resp.upper().startswith('Y'):
|
|
837
861
|
print(f'Quitting. Job {job.index_number} has not been deleted and job file is unmodified.')
|
|
838
862
|
return 1
|
|
@@ -885,11 +909,61 @@ class UrlwatchCommand:
|
|
|
885
909
|
print(f'Job not found: "{self.urlwatch_config.change_location[0]}".')
|
|
886
910
|
return 1
|
|
887
911
|
message = 'Do you want me to update the jobs file (remarks will be lost)? [y/N] '
|
|
888
|
-
|
|
912
|
+
try:
|
|
913
|
+
resp = input(message)
|
|
914
|
+
except EOFError: # no input available (e.g. run by cron, or stdin redirected from the null device)
|
|
915
|
+
resp = ''
|
|
916
|
+
if not resp.lower().startswith('y'):
|
|
889
917
|
print(f'Please manually update the jobs file by replacing "{old_loc}" with "{new_loc}".')
|
|
890
918
|
else:
|
|
891
919
|
self.urlwatcher.jobs_storage.save(self.urlwatcher.jobs)
|
|
892
920
|
|
|
921
|
+
for query, enable in ((self.urlwatch_config.enable, True), (self.urlwatch_config.disable, False)):
|
|
922
|
+
if query is not None and self._set_job_enabled(query, enable):
|
|
923
|
+
return 1
|
|
924
|
+
|
|
925
|
+
return 0
|
|
926
|
+
|
|
927
|
+
def _set_job_enabled(self, query: str, enable: bool) -> int:
|
|
928
|
+
"""Enable or disable a job (urlwatch's --enable and --disable) by setting its 'enabled' directive and
|
|
929
|
+
rewriting the jobs file, which loses its remarks.
|
|
930
|
+
|
|
931
|
+
:param query: The job's index number, name or URL/command.
|
|
932
|
+
:param enable: True to enable the job, False to disable it.
|
|
933
|
+
:returns: 0 if successful (including when the job already was in the state requested), 1 otherwise.
|
|
934
|
+
"""
|
|
935
|
+
verb = 'enable' if enable else 'disable'
|
|
936
|
+
try:
|
|
937
|
+
job = self._find_job(query)
|
|
938
|
+
except ValueError as e:
|
|
939
|
+
print(e)
|
|
940
|
+
return 1
|
|
941
|
+
config = self.urlwatcher.config_storage.config
|
|
942
|
+
if job.with_defaults(config).is_enabled() == enable:
|
|
943
|
+
print(f'Job {job.index_number} is already {verb}d: {job.get_location()}. The jobs file is unmodified.')
|
|
944
|
+
return 0
|
|
945
|
+
if sys.__stdin__ and sys.__stdin__.isatty():
|
|
946
|
+
print(
|
|
947
|
+
f'WARNING: About to {verb} {job.get_indexed_location()}.\n'
|
|
948
|
+
' Job file will be overwritten and all remarks lost.\n'
|
|
949
|
+
)
|
|
950
|
+
try:
|
|
951
|
+
resp = input(" Please enter 'Y' to proceed: ")
|
|
952
|
+
except EOFError: # e.g. stdin redirected from NUL on Windows, which isatty() reports as a terminal
|
|
953
|
+
resp = ''
|
|
954
|
+
if not resp.upper().startswith('Y'):
|
|
955
|
+
print(f'Quitting. Job {job.index_number} has not been {verb}d and job file is unmodified.')
|
|
956
|
+
return 1
|
|
957
|
+
if enable:
|
|
958
|
+
# Remove the directive, unless the job would remain disabled by job_defaults
|
|
959
|
+
job.enabled = None
|
|
960
|
+
if not job.with_defaults(config).is_enabled():
|
|
961
|
+
job.enabled = True
|
|
962
|
+
else:
|
|
963
|
+
job.enabled = False
|
|
964
|
+
logger.info('Job %s: %sd (enabled: %s)', job.index_number, verb, job.enabled)
|
|
965
|
+
print(f'{verb.capitalize()}d job {job.index_number}: {job.get_location()}.')
|
|
966
|
+
self.urlwatcher.jobs_storage.save(self.urlwatcher.jobs)
|
|
893
967
|
return 0
|
|
894
968
|
|
|
895
969
|
def check_telegram_chats(self) -> None:
|
|
@@ -1227,7 +1301,13 @@ class UrlwatchCommand:
|
|
|
1227
1301
|
if self.urlwatch_config.dump_history:
|
|
1228
1302
|
self._exit(self.dump_history(self.urlwatch_config.dump_history))
|
|
1229
1303
|
|
|
1230
|
-
if
|
|
1304
|
+
if (
|
|
1305
|
+
self.urlwatch_config.add
|
|
1306
|
+
or self.urlwatch_config.delete
|
|
1307
|
+
or self.urlwatch_config.change_location
|
|
1308
|
+
or self.urlwatch_config.enable
|
|
1309
|
+
or self.urlwatch_config.disable
|
|
1310
|
+
):
|
|
1231
1311
|
self._exit(self.modify_urls())
|
|
1232
1312
|
|
|
1233
1313
|
if self.urlwatch_config.test_reporter and not self.urlwatch_config.joblist:
|
|
@@ -42,10 +42,12 @@ class CommandConfig(BaseConfig):
|
|
|
42
42
|
delete: str | None
|
|
43
43
|
delete_snapshot: str | None
|
|
44
44
|
detailed_versions: bool
|
|
45
|
+
disable: str | None
|
|
45
46
|
dump_history: str | None
|
|
46
47
|
edit: bool
|
|
47
48
|
edit_config: bool
|
|
48
49
|
edit_hooks: bool
|
|
50
|
+
enable: str | None
|
|
49
51
|
errors: str | None
|
|
50
52
|
features: bool
|
|
51
53
|
footnote: str | None
|
|
@@ -136,7 +138,7 @@ class CommandConfig(BaseConfig):
|
|
|
136
138
|
parser.add_argument(
|
|
137
139
|
'joblist',
|
|
138
140
|
nargs='*',
|
|
139
|
-
help=
|
|
141
|
+
help='JOB(S) to run (index number(s) as per --list, name(s) or URL(s)/command(s)) (default: run all jobs)',
|
|
140
142
|
metavar='JOB(S)',
|
|
141
143
|
)
|
|
142
144
|
parser.add_argument(
|
|
@@ -216,6 +218,16 @@ class CommandConfig(BaseConfig):
|
|
|
216
218
|
metavar='REGEX',
|
|
217
219
|
dest='list_jobs',
|
|
218
220
|
)
|
|
221
|
+
group.add_argument(
|
|
222
|
+
'--enable',
|
|
223
|
+
help='enable a JOB (by index, name or URL/command) by rewriting the jobs file, losing its remarks',
|
|
224
|
+
metavar='JOB',
|
|
225
|
+
)
|
|
226
|
+
group.add_argument(
|
|
227
|
+
'--disable',
|
|
228
|
+
help='disable a JOB (by index, name or URL/command) by rewriting the jobs file, losing its remarks',
|
|
229
|
+
metavar='JOB',
|
|
230
|
+
)
|
|
219
231
|
group.add_argument(
|
|
220
232
|
'--tags',
|
|
221
233
|
action='append',
|
|
@@ -234,8 +246,8 @@ class CommandConfig(BaseConfig):
|
|
|
234
246
|
'--test',
|
|
235
247
|
nargs='?',
|
|
236
248
|
const=True,
|
|
237
|
-
help='test a JOB (by index or URL/command) and show filtered output; if no JOB, check config and
|
|
238
|
-
'file(s) syntax for errors',
|
|
249
|
+
help='test a JOB (by index, name or URL/command) and show filtered output; if no JOB, check config and '
|
|
250
|
+
'jobs file(s) syntax for errors',
|
|
239
251
|
metavar='JOB',
|
|
240
252
|
dest='test_job',
|
|
241
253
|
)
|
|
@@ -255,8 +267,8 @@ class CommandConfig(BaseConfig):
|
|
|
255
267
|
group.add_argument(
|
|
256
268
|
'--test-differ',
|
|
257
269
|
nargs='+',
|
|
258
|
-
help='show diff(s) using existing saved snapshots of a JOB (by index or URL/command); can be
|
|
259
|
-
'--test-reporter',
|
|
270
|
+
help='show diff(s) using existing saved snapshots of a JOB (by index, name or URL/command); can be '
|
|
271
|
+
'combined with --test-reporter',
|
|
260
272
|
metavar='JOB',
|
|
261
273
|
dest='test_differ',
|
|
262
274
|
)
|
|
@@ -270,7 +282,7 @@ class CommandConfig(BaseConfig):
|
|
|
270
282
|
)
|
|
271
283
|
group.add_argument(
|
|
272
284
|
'--dump-history',
|
|
273
|
-
help='print all saved changed snapshots for a JOB (by index or URL/command)',
|
|
285
|
+
help='print all saved changed snapshots for a JOB (by index, name or URL/command)',
|
|
274
286
|
metavar='JOB',
|
|
275
287
|
)
|
|
276
288
|
group.add_argument(
|
|
@@ -377,7 +389,7 @@ class CommandConfig(BaseConfig):
|
|
|
377
389
|
)
|
|
378
390
|
group.add_argument(
|
|
379
391
|
'--delete-snapshot',
|
|
380
|
-
help='delete the last saved changed snapshot of JOB (index or URL/command)',
|
|
392
|
+
help='delete the last saved changed snapshot of JOB (index, name or URL/command)',
|
|
381
393
|
metavar='JOB',
|
|
382
394
|
)
|
|
383
395
|
group.add_argument(
|
|
@@ -388,7 +400,7 @@ class CommandConfig(BaseConfig):
|
|
|
388
400
|
group.add_argument(
|
|
389
401
|
'--change-location',
|
|
390
402
|
nargs=2,
|
|
391
|
-
help='change the location of an existing JOB (index or URL/command)',
|
|
403
|
+
help='change the location of an existing JOB (index, name or URL/command)',
|
|
392
404
|
metavar=('JOB', 'NEW_LOCATION'),
|
|
393
405
|
)
|
|
394
406
|
|
|
@@ -437,7 +449,7 @@ class CommandConfig(BaseConfig):
|
|
|
437
449
|
)
|
|
438
450
|
group.add_argument(
|
|
439
451
|
'--delete',
|
|
440
|
-
# help='delete a job (by index or URL/command) [use --edit-jobs instead]',
|
|
452
|
+
# help='delete a job (by index, name or URL/command) [use --edit-jobs instead]',
|
|
441
453
|
help=argparse.SUPPRESS,
|
|
442
454
|
metavar='JOB',
|
|
443
455
|
)
|
|
@@ -234,6 +234,17 @@ class Html2TextFilter(FilterBase):
|
|
|
234
234
|
options = subfilter.copy()
|
|
235
235
|
method = options.pop('method', 'html2text')
|
|
236
236
|
|
|
237
|
+
if method == 'lynx': # urlwatch; lynx's options (e.g. width) mean nothing to the html2text package
|
|
238
|
+
ignored = f' and its options {", ".join(options)}, which are ignored' if options else ''
|
|
239
|
+
warnings.warn(
|
|
240
|
+
f"Filter html2text's method 'lynx' is no longer supported and the filter's default method is used "
|
|
241
|
+
f'instead: remove method{ignored} ({self.job.get_indexed_location()})',
|
|
242
|
+
DeprecationWarning,
|
|
243
|
+
stacklevel=1,
|
|
244
|
+
)
|
|
245
|
+
method = 'html2text'
|
|
246
|
+
options = {}
|
|
247
|
+
|
|
237
248
|
if method in {'html2text', 'pyhtml2text'}: # pythtml2text for backward compatibility
|
|
238
249
|
if method == 'pyhtml2text':
|
|
239
250
|
warnings.warn(
|
|
@@ -290,12 +301,6 @@ class Html2TextFilter(FilterBase):
|
|
|
290
301
|
stripped_tags = html.unescape(re.sub(r'<[^>]*>', '', data))
|
|
291
302
|
return '\n'.join((line.rstrip() for line in stripped_tags.splitlines() if line.strip() != '')), 'text/plain'
|
|
292
303
|
|
|
293
|
-
if method == 'lynx':
|
|
294
|
-
raise NotImplementedError(
|
|
295
|
-
f"Filter html2text's method 'lynx' is no longer supported; for similar results, use the filter without "
|
|
296
|
-
f'specifying a method. ({self.job.get_indexed_location()})'
|
|
297
|
-
)
|
|
298
|
-
|
|
299
304
|
raise ValueError(f"Unknown method {method} for filter 'html2text'. ({self.job.get_indexed_location()})")
|
|
300
305
|
|
|
301
306
|
|