Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow reporters to be specified multiple times #822

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The format mostly follows [Keep a Changelog](http://keepachangelog.com/en/1.0.0/
- New filter `re.findall` (Requested in #804 by f0sh, contributed in #805 by jamstah)
- Added tags to jobs and the ability to select them at the command line (#789, #824 by jamstah)
- New reporter: `gotify` (#823 by franco-righetti)
- Allow reporters to be specified multiple times (#822 by jamstah)

### Changed

Expand Down
28 changes: 28 additions & 0 deletions docs/source/reporters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,34 @@ If the notification does not work, check your configuration and/or add
the ``--verbose`` command-line option to show detailed debug logs.


Common options
--------------

You can use a list of configurations under a reporter type to report
different jobs with different configurations. You can select the jobs
for each reporter by using tags.

You can enable or disable a reporter by using the ``enabled`` option.

For example:

.. code:: yaml

telegram:
- bot_token: '999999999:3tOhy2CuZE0pTaCtszRfKpnagOG8IQbP5gf' # your bot api token
chat_id:
- '11111111'
- '22222222'
enabled: true
tags: [chat1]
- bot_token: '999999999:90jf403vnc09m0vi4s09t409jc09fj09sdc' # your bot api token
chat_id:
- '33333333'
- '44444444'
tags: [chat2]
enabled: true


Built-in reporters
------------------

Expand Down
5 changes: 4 additions & 1 deletion lib/urlwatch/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,10 @@ def set_error(job_state, message):
'Same Old, Same Old\n'))
report.error(set_error(build_job('Error Reporting', 'http://example.com/error', '', ''), 'Oh Noes!'))

report.finish_one(name)
reported = report.finish_one(name)

if not reported:
raise ValueError(f'Reporter not enabled: {name}')
Comment on lines +383 to +386
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
reported = report.finish_one(name)
if not reported:
raise ValueError(f'Reporter not enabled: {name}')
if not report.finish_one(name):
raise ValueError(f'Reporter not enabled: {name}')

..but is that changing behaviour from before?


sys.exit(0)

Expand Down
5 changes: 3 additions & 2 deletions lib/urlwatch/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def __init__(self, cache_storage, job):
self.timestamp = None
self.current_timestamp = None
self.exception = None
self.reported_count = 0
self.traceback = None
self.tries = 0
self.etag = None
Expand Down Expand Up @@ -214,10 +215,10 @@ def finish(self):
end = datetime.datetime.now()
duration = (end - self.start)

ReporterBase.submit_all(self, self.job_states, duration)
return ReporterBase.submit_all(self, self.job_states, duration)

def finish_one(self, name):
end = datetime.datetime.now()
duration = (end - self.start)

ReporterBase.submit_one(name, self, self.job_states, duration)
return ReporterBase.submit_one(name, self, self.job_states, duration)
4 changes: 2 additions & 2 deletions lib/urlwatch/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,8 @@ class Job(JobBase):
__required__ = ()
__optional__ = ('name', 'filter', 'max_tries', 'diff_tool', 'compared_versions', 'diff_filter', 'enabled', 'treat_new_as_changed', 'user_visible_url', 'tags')

def matching_tags(self, tags: Set[str]) -> Set[str]:
return self.tags & tags
def matching_tags(self, tags: Iterable[str]) -> Set[str]:
return self.tags.intersection(tags)

# determine if hyperlink "a" tag is used in HtmlReporter
def location_is_url(self):
Expand Down
10 changes: 9 additions & 1 deletion lib/urlwatch/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,5 +109,13 @@ def run_jobs(self):
run_jobs(self)

def close(self):
self.report.finish()
reported = self.report.finish()

if not reported:
logger.warning('No reporters enabled.')
Comment on lines +112 to +115
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like to do it like this mostly so that the "reported" local variable doesn't leak into the scope (this way, we can be sure that it isn't used below, since we don't give the expression result a name to begin with):

Suggested change
reported = self.report.finish()
if not reported:
logger.warning('No reporters enabled.')
if not self.report.finish():
logger.warning('No reporters enabled.')


for job_state in self.report.job_states:
if not job_state.reported_count:
logger.warning(f'Job {job_state.job.pretty_name()} was not reported on')

self.cache_storage.close()
59 changes: 35 additions & 24 deletions lib/urlwatch/reporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@


import asyncio
from collections.abc import Mapping
import difflib
import re
import email.utils
Expand Down Expand Up @@ -80,13 +81,20 @@
WDIFF_REMOVED_RE = r'[\[][-].*?[-][]]'


def filter_by_tags(job_states, tags):
if tags:
return [job_state for job_state in job_states if job_state.job.matching_tags(tags)]
return job_states


class ReporterBase(object, metaclass=TrackSubClasses):
__subclasses__ = {}

def __init__(self, report, config, job_states, duration):
def __init__(self, report, config, job_states, job_count_total, duration):
self.report = report
self.config = config
self.job_states = job_states
self.job_count_total = job_count_total
self.duration = duration

def get_signature(self):
Expand All @@ -96,8 +104,10 @@ def get_signature(self):
copyright=urlwatch.__copyright__),
'Website: {url}'.format(url=urlwatch.__url__),
'Support urlwatch development: https://github.com/sponsors/thp',
'watched {count} URLs in {duration} seconds'.format(count=len(self.job_states),
duration=self.duration.seconds),
'watched {total} URLs in {duration} seconds'.format(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to show here that something is filtered down? e.g. watched 5 or 9 URLs in 12.34 seconds?

count=len(self.job_states),
total=self.job_count_total,
duration=self.duration.seconds),
)

def convert(self, othercls):
Expand All @@ -106,7 +116,7 @@ def convert(self, othercls):
else:
config = {}

return othercls(self.report, config, self.job_states, self.duration)
return othercls(self.report, config, self.job_states, self.job_count_total, self.duration)

@classmethod
def get_base_config(cls, report):
Expand All @@ -123,35 +133,36 @@ def reporter_documentation(cls):

@classmethod
def submit_one(cls, name, report, job_states, duration):
any_enabled = False
subclass = cls.__subclasses__[name]
cfg = report.config['report'].get(name, {'enabled': False})
if cfg['enabled']:
base_config = subclass.get_base_config(report)
if base_config.get('separate', False):
for job_state in job_states:
subclass(report, cfg, [job_state], duration).submit()
else:
subclass(report, cfg, job_states, duration).submit()
else:
raise ValueError('Reporter not enabled: {name}'.format(name=name))
cfgs = report.config['report'].get(name, {'enabled': False})
if isinstance(cfgs, Mapping):
cfgs = [cfgs]

@classmethod
def submit_all(cls, report, job_states, duration):
any_enabled = False
for name, subclass in cls.__subclasses__.items():
cfg = report.config['report'].get(name, {})
for cfg in cfgs:
if cfg.get('enabled', False):
any_enabled = True
logger.info('Submitting with %s (%r)', name, subclass)
base_config = subclass.get_base_config(report)
matching_job_states = filter_by_tags(job_states, cfg.get("tags", []))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can get away with this, and then not having to check for the empty tags in filter_by_tags():

Suggested change
matching_job_states = filter_by_tags(job_states, cfg.get("tags", []))
if tags := cfg.get("tags"):
job_states = filter_by_tags(job_states, tags)

if base_config.get('separate', False):
for job_state in job_states:
subclass(report, cfg, [job_state], duration).submit()
for job_state in matching_job_states:
subclass(report, cfg, [job_state], len(job_states), duration).submit()
job_state.reported_count = job_state.reported_count + 1
else:
subclass(report, cfg, job_states, duration).submit()
subclass(report, cfg, matching_job_states, len(job_states), duration).submit()
Comment on lines +150 to +153
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't checked what's the right way, but curiously it's always len(job_states), not len(matching_job_states), is that correct/intended or a bug?

for job_state in matching_job_states:
job_state.reported_count = job_state.reported_count + 1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
job_state.reported_count = job_state.reported_count + 1
job_state.reported_count += 1


return any_enabled

@classmethod
def submit_all(cls, report, job_states, duration):
any_enabled = False
for name in cls.__subclasses__.keys():
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might also work:

Suggested change
for name in cls.__subclasses__.keys():
for name in cls.__subclasses__:

any_enabled = any_enabled | ReporterBase.submit_one(name, report, job_states, duration)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
any_enabled = any_enabled | ReporterBase.submit_one(name, report, job_states, duration)
any_enabled |= ReporterBase.submit_one(name, report, job_states, duration)


if not any_enabled:
logger.warning('No reporters enabled.')
return any_enabled

def submit(self):
raise NotImplementedError()
Expand Down
Loading