Skip to content

Commit c3cb2a8

Browse files
committed
Add a cli command to sync resources
1 parent 84d4223 commit c3cb2a8

2 files changed

Lines changed: 124 additions & 8 deletions

File tree

ckanext/activityinfo/cli/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ def resources():
3535
# ckan activityinfo resources update-activity-info-resource -r xxxxx -u username
3636
# The user should have permissions to access the ActivityInfo API (API key) and the resource to update
3737
resources.add_command(cli_resources.update_activityinfo_resource)
38+
39+
# ckan activityinfo resources sync-auto-updates [--dry-run] [-v]
40+
# Find and update all resources due for automatic update (meant for cron)
41+
resources.add_command(cli_resources.sync_auto_updates)
Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import logging
2+
from datetime import datetime, timezone
3+
24
import click
5+
from ckan.plugins import toolkit
36
from ckanext.activityinfo.jobs.download import download_activityinfo_resource
7+
from ckanext.activityinfo.utils import get_resources_due_for_auto_update
8+
9+
10+
def _setup_download_logging(verbose=False):
11+
"""Set up a logging handler that forwards download logs to click.echo.
12+
13+
Returns (handler, logger) so the caller can remove the handler when done.
14+
"""
15+
download_logger = logging.getLogger('ckanext.activityinfo.jobs.download')
16+
handler = logging.Handler()
17+
handler.emit = lambda record: click.echo(f" {handler.format(record)}")
18+
handler.setFormatter(logging.Formatter('%(message)s'))
19+
handler.setLevel(logging.DEBUG if verbose else logging.INFO)
20+
download_logger.addHandler(handler)
21+
download_logger.setLevel(handler.level)
22+
return handler, download_logger
423

524

625
@click.command(
@@ -13,18 +32,111 @@
1332
def update_activityinfo_resource(resource_id, user_name, verbose):
1433
""" Update a single ActivityInfo resource. """
1534

16-
# Temporarily attach a handler that forwards log messages to click.echo
17-
download_logger = logging.getLogger('ckanext.activityinfo.jobs.download')
18-
handler = logging.Handler()
19-
handler.emit = lambda record: click.echo(handler.format(record))
20-
handler.setFormatter(logging.Formatter('%(message)s'))
21-
handler.setLevel(logging.DEBUG if verbose else logging.INFO)
22-
download_logger.addHandler(handler)
23-
download_logger.setLevel(handler.level)
35+
handler, download_logger = _setup_download_logging(verbose)
2436

2537
click.echo('Updating ActivityInfo resource')
2638
try:
2739
download_activityinfo_resource(resource_id=resource_id, user=user_name)
2840
click.echo('ActivityInfo resource updated successfully')
2941
finally:
3042
download_logger.removeHandler(handler)
43+
44+
45+
@click.command(
46+
'sync-auto-updates',
47+
short_help='Sync all ActivityInfo resources due for automatic update'
48+
)
49+
@click.option('-v', '--verbose', count=True)
50+
@click.option(
51+
'--dry-run', is_flag=True, default=False,
52+
help='Show what would be updated without actually running updates'
53+
)
54+
def sync_auto_updates(verbose, dry_run):
55+
"""Find and update all ActivityInfo resources due for automatic update.
56+
57+
This command is meant to be run from cron. It checks all resources with
58+
activityinfo_auto_update set to 'daily' or 'weekly', verifies timing
59+
and run limits, and triggers downloads for those that are due.
60+
61+
Each resource is updated using the CKAN user who originally created it
62+
(stored in the activityinfo_user field).
63+
"""
64+
click.echo("Checking for ActivityInfo resources due for auto-update...")
65+
66+
due_resources = get_resources_due_for_auto_update()
67+
68+
if not due_resources:
69+
click.echo("No resources due for update.")
70+
return
71+
72+
click.echo(f"Found {len(due_resources)} resource(s) due for update.")
73+
74+
if dry_run:
75+
for res in due_resources:
76+
count = res.get('activityinfo_auto_update_count', 0)
77+
max_runs = res.get('activityinfo_auto_update_runs', 1)
78+
user = res.get('activityinfo_user', '?')
79+
click.echo(
80+
f" [DRY RUN] {res['id']} - "
81+
f"{res.get('activityinfo_form_label', '?')} "
82+
f"({res.get('activityinfo_auto_update')}, "
83+
f"run {count}/{max_runs}, user: {user})"
84+
)
85+
return
86+
87+
enqueued = 0
88+
failed = 0
89+
skipped = 0
90+
91+
for res in due_resources:
92+
resource_id = res['id']
93+
form_label = res.get('activityinfo_form_label', resource_id)
94+
current_count = int(res.get('activityinfo_auto_update_count', 0) or 0)
95+
max_runs = int(res.get('activityinfo_auto_update_runs', 1) or 1)
96+
user_name = res.get('activityinfo_user')
97+
98+
if not user_name:
99+
click.echo(
100+
f"\nSkipping: {form_label} ({resource_id}) "
101+
f"- no activityinfo_user set"
102+
)
103+
skipped += 1
104+
continue
105+
106+
click.echo(
107+
f"\nUpdating: {form_label} ({resource_id}) "
108+
f"- run {current_count + 1}/{max_runs}, user: {user_name}"
109+
)
110+
111+
try:
112+
result = toolkit.get_action('act_info_update_resource_file')(
113+
{'user': user_name, 'ignore_auth': True},
114+
{'resource_id': resource_id}
115+
)
116+
117+
# Update the counter and timestamp now that the job is enqueued
118+
# No matter if the job succeeds or fails, we count this as a run to avoid infinite retries on failures
119+
# Errors will be registered with the activityinfo_error resource extra field
120+
now_iso = datetime.now(timezone.utc).isoformat()
121+
toolkit.get_action('resource_patch')(
122+
{'user': user_name, 'ignore_auth': True},
123+
{
124+
'id': resource_id,
125+
'activityinfo_last_updated': now_iso,
126+
'activityinfo_auto_update_count': current_count + 1,
127+
}
128+
)
129+
130+
job_id = result.get('job_id', '?')
131+
click.echo(
132+
f" OK - job {job_id} enqueued "
133+
f"(run {current_count + 1}/{max_runs})"
134+
)
135+
enqueued += 1
136+
137+
except Exception as e:
138+
click.echo(f" FAILED - {e}", err=True)
139+
failed += 1
140+
continue
141+
142+
click.echo(f"\nSync complete: {enqueued} enqueued, {failed} failed, {skipped} skipped.")

0 commit comments

Comments
 (0)