webchanges 3.31.4__tar.gz → 3.33.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {webchanges-3.31.4/webchanges.egg-info → webchanges-3.33.0}/PKG-INFO +6 -3
  2. {webchanges-3.31.4 → webchanges-3.33.0}/README.rst +2 -0
  3. {webchanges-3.31.4 → webchanges-3.33.0}/pyproject.toml +88 -123
  4. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/__init__.py +2 -2
  5. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/_vendored/headers.py +5 -5
  6. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/cli.py +10 -7
  7. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/command.py +71 -38
  8. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/config.py +1 -2
  9. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/differs.py +53 -32
  10. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/filters.py +21 -31
  11. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/handler.py +57 -78
  12. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/jobs.py +397 -277
  13. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/reporters.py +27 -28
  14. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/storage.py +55 -30
  15. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/util.py +16 -7
  16. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/worker.py +21 -12
  17. {webchanges-3.31.4 → webchanges-3.33.0/webchanges.egg-info}/PKG-INFO +6 -3
  18. {webchanges-3.31.4 → webchanges-3.33.0}/LICENSE.md +0 -0
  19. {webchanges-3.31.4 → webchanges-3.33.0}/MANIFEST.in +0 -0
  20. {webchanges-3.31.4 → webchanges-3.33.0}/requirements.txt +0 -0
  21. {webchanges-3.31.4 → webchanges-3.33.0}/setup.cfg +0 -0
  22. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/__main__.py +0 -0
  23. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/_vendored/__init__.py +0 -0
  24. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/_vendored/packaging_version.py +0 -0
  25. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/mailer.py +0 -0
  26. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/main.py +0 -0
  27. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/py.typed +0 -0
  28. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges/storage_minidb.py +0 -0
  29. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges.egg-info/SOURCES.txt +0 -0
  30. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges.egg-info/dependency_links.txt +0 -0
  31. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges.egg-info/entry_points.txt +0 -0
  32. {webchanges-3.31.4 → webchanges-3.33.0}/webchanges.egg-info/requires.txt +0 -0
  33. {webchanges-3.31.4 → webchanges-3.33.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.31.4
3
+ Version: 3.33.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>
@@ -88,11 +88,12 @@ Classifier: Operating System :: OS Independent
88
88
  Classifier: Programming Language :: Python
89
89
  Classifier: Programming Language :: Python :: 3
90
90
  Classifier: Programming Language :: Python :: 3 :: Only
91
- Classifier: Programming Language :: Python :: 3.10
92
91
  Classifier: Programming Language :: Python :: 3.11
93
92
  Classifier: Programming Language :: Python :: 3.12
94
93
  Classifier: Programming Language :: Python :: 3.13
95
94
  Classifier: Programming Language :: Python :: 3.14
95
+ Classifier: Programming Language :: Python :: Free Threading
96
+ Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient
96
97
  Classifier: Programming Language :: Python :: Implementation :: CPython
97
98
  Classifier: Topic :: Internet
98
99
  Classifier: Topic :: Internet :: WWW/HTTP
@@ -100,7 +101,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
100
101
  Classifier: Topic :: System :: Monitoring
101
102
  Classifier: Topic :: Utilities
102
103
  Classifier: Typing :: Typed
103
- Requires-Python: >=3.10
104
+ Requires-Python: >=3.11
104
105
  Description-Content-Type: text/x-rst
105
106
  License-File: LICENSE.md
106
107
  Requires-Dist: colorama; sys_platform == "win32"
@@ -189,6 +190,8 @@ For the best experience, use the current version of `Python <https://www.python.
189
190
  older Python versions for 3 years after they're replaced by a newer one; we just ask that you use the most up-to-date
190
191
  bug and security fix release from that older version.
191
192
 
193
+ While **webchanges** supports free-threated Python, certain optional dependencies may not.
194
+
192
195
  For Generative AI summaries (BETA), you need a free `API Key from Google Cloud AI Studio
193
196
  <https://aistudio.google.com/app/apikey>`__ (see `here
194
197
  <https://webchanges.readthedocs.io/en/stable/differs.html#ai-google>`__).
@@ -22,6 +22,8 @@ For the best experience, use the current version of `Python <https://www.python.
22
22
  older Python versions for 3 years after they're replaced by a newer one; we just ask that you use the most up-to-date
23
23
  bug and security fix release from that older version.
24
24
 
25
+ While **webchanges** supports free-threated Python, certain optional dependencies may not.
26
+
25
27
  For Generative AI summaries (BETA), you need a free `API Key from Google Cloud AI Studio
26
28
  <https://aistudio.google.com/app/apikey>`__ (see `here
27
29
  <https://webchanges.readthedocs.io/en/stable/differs.html#ai-google>`__).
@@ -6,9 +6,9 @@
6
6
 
7
7
  [build-system]
8
8
  # Minimum requirements for the build system to execute.
9
- requires = ['setuptools'] # PEP 508 specifications.
9
+ requires = ['argparse-manpage[setuptools]'] # PEP 508 specifications.
10
10
  # Setuptools specification
11
- build-backend = "setuptools.build_meta"
11
+ build-backend = 'setuptools.build_meta'
12
12
 
13
13
  [project]
14
14
  # See https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
@@ -16,7 +16,7 @@ dynamic = ['version', 'dependencies']
16
16
  name = 'webchanges'
17
17
  description = 'Web Changes Delivered. AI-Summarized. Totally Anonymous.'
18
18
  readme = { file = 'README.rst', content-type = 'text/x-rst' }
19
- requires-python = '>=3.10'
19
+ requires-python = '>=3.11'
20
20
  license = { file = 'LICENSE.md' }
21
21
  authors = [{ name = 'Mike Borsetti', email = 'mike+webchanges@borsetti.com' }]
22
22
  maintainers = [
@@ -36,11 +36,12 @@ classifiers = [
36
36
  'Programming Language :: Python',
37
37
  'Programming Language :: Python :: 3',
38
38
  'Programming Language :: Python :: 3 :: Only',
39
- 'Programming Language :: Python :: 3.10',
40
39
  'Programming Language :: Python :: 3.11',
41
40
  'Programming Language :: Python :: 3.12',
42
41
  'Programming Language :: Python :: 3.13',
43
42
  'Programming Language :: Python :: 3.14',
43
+ 'Programming Language :: Python :: Free Threading',
44
+ 'Programming Language :: Python :: Free Threading :: 4 - Resilient',
44
45
  'Programming Language :: Python :: Implementation :: CPython',
45
46
  'Topic :: Internet',
46
47
  'Topic :: Internet :: WWW/HTTP',
@@ -88,6 +89,7 @@ xmpp = ['aioxmpp']
88
89
  redis = ['redis']
89
90
  requests = ['requests']
90
91
  safe_password = ['keyring']
92
+ # all
91
93
  all = [
92
94
  'webchanges[use_browser,beautify,bs4,html5lib,ical2text,jq,ocr,pdf2text,pypdf_crypto,deepdiff_xml,imagediff,matrix,pushbullet,pushover,xmpp,redis,requests,safe_password]',
93
95
  ]
@@ -109,6 +111,10 @@ dependencies = { file = 'requirements.txt' }
109
111
  [tool.setuptools.package-data]
110
112
  'webchanges' = ['py.typed']
111
113
 
114
+ # -------------------------- manpages --------------------------
115
+ # https://github.com/praiskup/argparse-manpage/blob/main/README.md
116
+ [tool.build_manpages]
117
+ manpages = ['man/webchanges.1:function=get_parser:pyfile=get_parser']
112
118
 
113
119
  # -------------------------- coverage --------------------------
114
120
  [tool.coverage.run]
@@ -170,60 +176,6 @@ exclude_lines = [
170
176
  # ignore_errors = true
171
177
 
172
178
 
173
- # -------------------------- mypy --------------------------
174
- [tool.mypy]
175
- # Static Typing for Python
176
- # Runs as part of pre-commit
177
- # Config file documentation at https://mypy.readthedocs.io/en/stable/config_file.html
178
-
179
- # Disables import discovery of namespace packages (see PEP 420)
180
- namespace_packages = true
181
-
182
- # Specifies the Python version used to parse and check the target program.
183
- # python_version = 3.12
184
-
185
- # Suppresses error messages about imports that cannot be resolved.
186
- ignore_missing_imports = true
187
-
188
- # Disallows calling functions without type annotations from functions with type annotations.
189
- disallow_untyped_calls = false
190
-
191
- # Disallows defining functions without type annotations or with incomplete type annotations.
192
- disallow_untyped_defs = true
193
-
194
- # Reports an error whenever a function with type annotations is decorated with a decorator without annotations.
195
- disallow_untyped_decorators = true
196
-
197
- # Warns about casting an expression to its inferred type.
198
- warn_redundant_casts = true
199
-
200
- # Warns about unneeded # type: ignore comments.
201
- # May behave differently in GitHub Actions than it does on Windows.
202
- # warn_unused_ignores = true
203
-
204
- # Shows a warning when returning a value with type Any from a function declared with a non-Any return type.
205
- warn_return_any = true
206
-
207
- # Shows a warning when encountering any code inferred to be unreachable or redundant after performing type analysis.
208
- warn_unreachable = false
209
-
210
- # Enables additional checks that are technically correct but may be impractical in real code.
211
- extra_checks = true
212
-
213
- # Shows documentation link to corresponding error code.
214
- show_error_code_links = true
215
-
216
- # Use visually nicer output in error messages: use soft word wrap, show source code snippets, and show error location
217
- # markers.
218
- pretty = true
219
-
220
- # Use an SQLite database to store the cache.
221
- sqlite_cache = true
222
-
223
- # Warns about per-module sections in the config file that do not match any files processed when invoking mypy.
224
- warn_unused_configs = true
225
-
226
-
227
179
  # -------------------------- rstcheck --------------------------
228
180
  [tool.rstcheck]
229
181
  # Checks syntax of reStructuredText and code blocks nested within it.
@@ -240,12 +192,15 @@ report_level = 'WARNING'
240
192
  # Testing framework
241
193
  # Config file documentation at https://docs.pytest.org/en/stable/reference/reference.html#ini-options-ref
242
194
 
243
- log_auto_indent = true
195
+ # log_auto_indent = 2
244
196
  # Enable log display during test run (aka "live logging" https://docs.pytest.org/en/stable/logging.html#live-logs)
245
- log_cli = true
246
- minversion = '8.4.1'
197
+ # log_cli = true
198
+ minversion = '9.0.2'
247
199
  testpaths = ['tests']
248
200
 
201
+ # pytest-playwright
202
+ # addopts = ['--browser', 'chromium', '--browser-channel', 'chrome']
203
+
249
204
  # Adds pytest-cov functionality (see https://pytest-cov.readthedocs.io/en/latest/config.html)
250
205
  # Note: --cov moved to .github/workflows/ci-cd.yaml and tox.ini due to interference with PyCharm breakpoints (see
251
206
  # https://github.com/pytest-dev/pytest-cov/issues/131) and to enable running tox --parallel
@@ -258,10 +213,7 @@ testpaths = ['tests']
258
213
  # Config file documentation at https://docs.astral.sh/ruff/configuration/ and https://docs.astral.sh/ruff/settings/
259
214
 
260
215
  # File patterns to omit from formatting and linting, in addition to those specified by exclude.
261
- extend-exclude = [
262
- "webchanges/storage_minidb.py",
263
- "webchanges/_vendored",
264
- ]
216
+ extend-exclude = ['webchanges/storage_minidb.py', 'webchanges/_vendored']
265
217
 
266
218
  # By default, Ruff will discover files matching *.py, *.pyi, *.ipynb, or pyproject.toml.
267
219
  # Include additional files
@@ -271,83 +223,87 @@ extend-exclude = [
271
223
  line-length = 120
272
224
 
273
225
  # Target Python version
274
- # target-version = "py311" # Commented out to infer from [project] requires-python
226
+ # target-version = 'py311' # Commented out to infer from [project] requires-python
275
227
 
276
228
  [tool.ruff.lint]
277
229
  # By default, Ruff enables Flake8's F rules, along with a subset of the E rules
278
230
  # Enable rules not enabled by default, and ignore specific rules.
279
231
  select = [
280
- "ANN", # flake8-annotations
281
- "S", # flake8-bandit
282
- "BLE", # flake8-blind-except
283
- "B", # flake8-bugbear
284
- "A", # flake8-builtin
285
- "C4", # flake8-comprehensions
286
- "DTZ", # flake8-datetimez
287
- # "EM", # flake8-errmsg # TODO
288
- "FA", # flake8-future-annotations
289
- "INT", # flake8-gettext
290
- "ISC", # flake8-implicit-str-concat
291
- "LOG", # flake8-logging
292
- # "G", # flake8-logging-format # TODO
293
- "PIE", # flake8-pie
294
- "PYI", # flake8-pyi
295
- "PT", # flake8-pytest-style
296
- "Q", # flake8-quotes
297
- "RSE", # flake8-raise
298
- "RET", # flake8-return
299
- "SIM", # flake8-simplify
300
- "TID", # flake8-tidy-imports
301
- "TD", # flake8-todos
302
- "TC", # flake8-type-checking
303
- # "ARG", # flake8-unused-arguments # TODO
304
- "PTH", # flake8-use-pathlib
305
- # "FLY", # flynt # TODO
306
- "I", # isort
307
- "C90", # mccabe
308
- "N", # pep8-naming
309
- "PERF", # Perflint
310
- "E", # pycodestyle errors
311
- "W", # pycodestyle warnings
312
- # "DOC", # pydoclint # TODO
313
- # "D", # pydocstyle # TODO
314
- "F", # Pyflakes
315
- # "PL", # Pylint # TODO
316
- "FURB", # refurb
317
- "RUF", # Ruff-specific rules
318
- # "TRY", # tryceratops # TODO
232
+ 'ANN', # flake8-annotations
233
+ 'S', # flake8-bandit
234
+ 'BLE', # flake8-blind-except
235
+ 'B', # flake8-bugbear
236
+ 'A', # flake8-builtin
237
+ 'C4', # flake8-comprehensions
238
+ 'DTZ', # flake8-datetimez
239
+ # 'EM', # flake8-errmsg # TODO
240
+ 'FA', # flake8-future-annotations
241
+ 'INT', # flake8-gettext
242
+ 'ISC', # flake8-implicit-str-concat
243
+ 'LOG', # flake8-logging
244
+ # 'G', # flake8-logging-format # Prefer f-string to lazy `%` formatting and OK with '+'
245
+ 'PIE', # flake8-pie
246
+ 'PYI', # flake8-pyi
247
+ 'PT', # flake8-pytest-style
248
+ 'Q', # flake8-quotes
249
+ 'RSE', # flake8-raise
250
+ 'RET', # flake8-return
251
+ 'SIM', # flake8-simplify
252
+ 'TID', # flake8-tidy-imports
253
+ 'TD', # flake8-todos
254
+ 'TC', # flake8-type-checking
255
+ # 'ARG', # flake8-unused-arguments # Code loses clarity
256
+ 'PTH', # flake8-use-pathlib
257
+ 'FLY', # flynt
258
+ 'I', # isort
259
+ 'C90', # mccabe
260
+ 'N', # pep8-naming
261
+ 'PERF', # Perflint
262
+ 'E', # pycodestyle errors
263
+ 'W', # pycodestyle warnings
264
+ # 'DOC', # pydoclint # TODO
265
+ # 'D', # pydocstyle # TODO
266
+ 'F', # Pyflakes
267
+ # 'PL', # Pylint # TODO
268
+ 'FURB', # refurb
269
+ 'RUF', # Ruff-specific rules
270
+ # 'TRY', # tryceratops # TODO
319
271
  ]
320
272
 
321
273
  ignore = [
322
- "PT011", # pytest.raises({exception}) is too broad, set the match parameter or use a more specific exception
323
- "PT030", # pytest.warns({warning}) is too broad, set the match parameter or use a more specific warning
324
- "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
325
- "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass
326
- "SIM115", # Use a context manager for opening files
327
- "TD002", # Missing author in TODO
328
- "TD003", # Missing issue link for this TODO
274
+ 'FLY002', # Consider f-string instead of string join
275
+ # 'G003', # Logging statement uses `+`'
276
+ # 'G004', # Logging statement uses f-string (Prefer f-string to lazy `%` formatting)
277
+ 'PLC0415', # `import` should be at the top-level of a file
278
+ 'PT011', # pytest.raises({exception}) is too broad, set the match parameter or use a more specific exception
279
+ 'PT030', # pytest.warns({warning}) is too broad, set the match parameter or use a more specific warning
280
+ 'RUF012', # Mutable class attributes should be annotated with `typing.ClassVar`
281
+ 'SIM105', # Use contextlib.suppress({exception}) instead of try-except-pass
282
+ 'SIM115', # Use a context manager for opening files
283
+ 'TD002', # Missing author in TODO
284
+ 'TD003', # Missing issue link for this TODO
329
285
  ]
330
286
 
331
287
  # Allow fix for all enabled rules (when `--fix`) is provided.
332
- fixable = ["ALL"]
288
+ fixable = ['ALL']
333
289
  unfixable = []
334
290
 
335
291
  # Allow unused variables when underscore-prefixed.
336
- dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
292
+ dummy-variable-rgx = '^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$'
337
293
 
338
294
  [tool.ruff.lint.flake8-annotations]
339
- allow-star-arg-any = true # TODO
295
+ allow-star-arg-any = true # TODO
340
296
 
341
297
  [tool.ruff.lint.flake8-quotes]
342
- inline-quotes = "single"
298
+ inline-quotes = 'single'
343
299
 
344
300
  [tool.ruff.lint.mccabe]
345
301
  # Flag errors (`C901`) whenever the complexity level exceeds below.
346
302
  max-complexity = 30
347
303
 
348
304
  [tool.ruff.lint.per-file-ignores]
349
- "tests/*" = [
350
- "S101", # Use of `assert` detected
305
+ 'tests/*' = [
306
+ 'S101', # Use of `assert` detected
351
307
  ]
352
308
 
353
309
  [tool.ruff.lint.pydoclint]
@@ -355,7 +311,7 @@ max-complexity = 30
355
311
  ignore-one-line-docstrings = true
356
312
 
357
313
  [tool.ruff.lint.pydocstyle]
358
- convention = "google"
314
+ convention = 'google'
359
315
 
360
316
  [tool.ruff.format]
361
317
  # Enable the formatter, which is a drop-in replacement for Black.
@@ -365,16 +321,16 @@ convention = "google"
365
321
  exclude = []
366
322
 
367
323
  # Use single quotes for strings.
368
- quote-style = "single"
324
+ quote-style = 'single'
369
325
 
370
326
  # Like Black, indent with spaces, rather than tabs.
371
- indent-style = "space"
327
+ indent-style = 'space'
372
328
 
373
329
  # Like Black, respect magic trailing commas.
374
330
  skip-magic-trailing-comma = false
375
331
 
376
332
  # Like Black, automatically detect the appropriate line ending.
377
- line-ending = "auto"
333
+ line-ending = 'auto'
378
334
 
379
335
  # Enable auto-formatting of code examples in docstrings. Markdown,
380
336
  # reStructuredText code/literal blocks and doctests are all supported.
@@ -388,4 +344,13 @@ docstring-code-format = true
388
344
  #
389
345
  # This only has an effect when the `docstring-code-format` setting is
390
346
  # enabled.
391
- docstring-code-line-length = "dynamic"
347
+ docstring-code-line-length = 'dynamic'
348
+
349
+ # -------------------------- ty --------------------------
350
+ # Config file documentation at https://docs.astral.sh/ty/reference/configuration/
351
+
352
+ [tool.ty.environment]
353
+ extra-paths = ['./webchanges']
354
+
355
+ [tool.ty.src]
356
+ exclude = ['./webchanges/storage_minidb.py']
@@ -12,7 +12,7 @@ supported services. Can check the output of local commands as well.
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
- __min_python_version__ = (3, 10) # minimum version of Python required to run; supported until fall 2025
15
+ __min_python_version__ = (3, 11) # minimum version of Python required to run; 3.11 supported until fall 2026
16
16
 
17
17
 
18
18
  __project_name__ = str(__package__)
@@ -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.31.4'
25
+ __version__ = '3.33.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
  )
@@ -50,7 +50,7 @@ def to_str(value: str | bytes, encoding: str = 'utf-8') -> str:
50
50
 
51
51
 
52
52
  def to_bytes_or_str(value: str, match_type_of: typing.AnyStr) -> typing.AnyStr:
53
- return value if isinstance(match_type_of, str) else value.encode()
53
+ return value if isinstance(match_type_of, str) else value.encode() # ty:ignore[invalid-return-type]
54
54
 
55
55
 
56
56
  # from https://github.com/encode/httpx/blob/master/httpx/_models.py
@@ -81,8 +81,8 @@ def _obfuscate_sensitive_headers(
81
81
  items: typing.Iterable[tuple[typing.AnyStr, typing.AnyStr]],
82
82
  ) -> typing.Iterator[tuple[typing.AnyStr, typing.AnyStr]]:
83
83
  for k, v in items:
84
- if to_str(k.lower()) in SENSITIVE_HEADERS:
85
- v = to_bytes_or_str('[secure]', match_type_of=v)
84
+ if to_str(k.lower()) in SENSITIVE_HEADERS: # ty:ignore[no-matching-overload, invalid-argument-type]
85
+ v = to_bytes_or_str('[secure]', match_type_of=v) # ty:ignore[invalid-argument-type]
86
86
  yield k, v
87
87
 
88
88
 
@@ -102,8 +102,8 @@ class Headers(typing.MutableMapping[str, str]):
102
102
  self._list = list(headers._list)
103
103
  elif isinstance(headers, Mapping):
104
104
  for k, v in headers.items():
105
- bytes_key = _normalize_header_key(k, encoding)
106
- bytes_value = _normalize_header_value(v, encoding)
105
+ bytes_key = _normalize_header_key(k, encoding) # ty:ignore[invalid-argument-type]
106
+ bytes_value = _normalize_header_value(v, encoding) # ty:ignore[invalid-argument-type]
107
107
  self._list.append((bytes_key, bytes_key.lower(), bytes_value))
108
108
  elif headers is not None:
109
109
  for k, v in headers:
@@ -24,9 +24,14 @@ from webchanges import __copyright__, __docs_url__, __min_python_version__, __pr
24
24
  from webchanges.config import CommandConfig
25
25
  from webchanges.util import file_ownership_checks, get_new_version_number, import_module_from_source
26
26
 
27
- # Ignore signal SIGPIPE ("broken pipe") for stdout (see https://github.com/thp/urlwatch/issues/77)
28
- if os.name != 'nt': # Windows does not have signal.SIGPIPE
27
+ # Restore the default system behavior for the SIGPIPE signal, which is ignored by Python by default.
28
+ # This prevents a BrokenPipeError when piping output to a command like `less` that may close the pipe before reading all
29
+ # of the output.
30
+ try:
29
31
  signal.signal(signal.SIGPIPE, signal.SIG_DFL) # type: ignore[attr-defined] # not defined in Windows
32
+ except AttributeError:
33
+ pass
34
+
30
35
 
31
36
  logger = logging.getLogger(__name__)
32
37
 
@@ -376,7 +381,7 @@ def main() -> None: # pragma: no cover
376
381
  python_version_warning()
377
382
 
378
383
  # Path where the config, jobs and hooks files are located
379
- if os.name != 'nt':
384
+ if sys.platform != 'win32':
380
385
  config_path = platformdirs.user_config_path(__project_name__) # typically ~/.config/{__project_name__}
381
386
  else:
382
387
  config_path = platformdirs.user_documents_path().joinpath(__project_name__)
@@ -471,10 +476,8 @@ def main() -> None: # pragma: no cover
471
476
  config_storage.load()
472
477
 
473
478
  # Setup database API
474
- database_engine = (
475
- command_config.database_engine or config_storage.config.get('database', {}).get('engine') or 'sqlite3'
476
- ) # "or 'sqlite3'" is not needed except for a mypy bug; same for the "or 4" below
477
- max_snapshots = command_config.max_snapshots or config_storage.config.get('database', {}).get('max_snapshots') or 4
479
+ database_engine = command_config.database_engine or config_storage.config.get('database', {}).get('engine')
480
+ max_snapshots = command_config.max_snapshots or config_storage.config.get('database', {}).get('max_snapshots')
478
481
  if database_engine == 'sqlite3':
479
482
  ssdb_storage: SsdbStorage = SsdbSQLite3Storage(command_config.ssdb_file, max_snapshots) # storage.py
480
483
  elif any(str(command_config.ssdb_file).startswith(prefix) for prefix in ('redis://', 'rediss://')):