• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

AntiCompositeNumber / iNaturalistReviewer / 6749837206

03 Nov 2023 08:22PM UTC coverage: 66.872% (+1.1%) from 65.741%
6749837206

push

github

AntiCompositeNumber
Upgrade to python3.11

338 of 505 branches covered (0.0%)

Branch coverage included in aggregate %.

637 of 953 relevant lines covered (66.84%)

0.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/inrcli.py
1
#!/usr/bin/env python3
2
# coding: utf-8
3
# SPDX-License-Identifier: Apache-2.0
4

5

6
# Copyright 2023 AntiCompositeNumber
7

8
# Licensed under the Apache License, Version 2.0 (the "License");
9
# you may not use this file except in compliance with the License.
10
# You may obtain a copy of the License at
11

12
#   http://www.apache.org/licenses/LICENSE-2.0
13

14
# Unless required by applicable law or agreed to in writing, software
15
# distributed under the License is distributed on an "AS IS" BASIS,
16
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
# See the License for the specific language governing permissions and
18
# limitations under the License.
19

20
import click
×
21
import pywikibot  # type: ignore
×
22
import pywikibot.bot  # type: ignore
×
23
import logging
×
24
import os
×
25
import sys
×
26
import difflib
×
27
import webbrowser
×
28
from typing import Sequence, Dict, Optional
×
29

30
os.environ["LOG_FILE"] = "stderr"
×
31
os.environ["LOG_LEVEL"] = "WARNING"
×
32

33
import inrbot  # noqa: E402
×
34

35
inrbot.run_override = True
×
36
inrbot.summary_tag = f"(inrcli {inrbot.__version__})"
×
37
site = inrbot.site
×
38
logger = logging.getLogger("manual")
×
39
ids: Dict[pywikibot.Page, Optional[inrbot.iNaturalistID]] = {}
×
40
auto_open = False
×
41

42
inrbot.config.update(
×
43
    {
44
        "fail_tag": "{{copyvio|License review not passed: "
45
        "iNaturalist author is using $review_license: $source_url}}\n",
46
        "fail_warn": "\n\n{{subst:Copyvionote |1=$filename "
47
        "|2=License review "
48
        "not passed: iNaturalist author is using $review_license: $source_url }} ~~~~",
49
        "review_summary": "Semi-automatic license review: "
50
        "$status $review_license $tag",
51
        "old_fail_warn": "\n\n{{subst:image permission|1=$filename}} "
52
        "License review not passed: iNaturalist author is "
53
        "using $review_license: $source_url. ~~~~",
54
        "use_wayback": False,
55
    }
56
)
57

58

59
class SkipFile(Exception):
×
60
    pass
×
61

62

63
def manual_compare(
×
64
    com_img: inrbot.CommonsImage, ina_img: inrbot.iNaturalistImage, **kwargs
65
):
66
    if ids.get(com_img.page, None) == ina_img.id and ina_img.id.type == "photos":
×
67
        return True
×
68
    else:
69
        return ask_compare(com_img, ina_img)
×
70

71

72
def ask_compare(com_img: inrbot.CommonsImage, ina_img: inrbot.iNaturalistImage):
×
73
    if click.confirm(f"Show {ina_img.id}?", default=False):
×
74
        com_img.image.show(title=com_img.page.title())
×
75
        ina_img.image.show(title=str(ina_img.id))
×
76
    res = click.confirm("Do these images match?", default=False)
×
77
    return res
×
78

79

80
inrbot.compare_methods.insert(0, ("manual", manual_compare))
×
81

82

83
class ManualCommonsPage(inrbot.CommonsPage):
×
84
    def __init__(self, *args, **kwargs):
×
85
        super().__init__(*args, **kwargs)
×
86

87
    def check_can_run(self) -> bool:
×
88
        """Determinies if the bot should run on this page and returns a bool."""
89
        page = self.page
×
90
        if (
×
91
            (page.title() in inrbot.skip)
92
            or (not page.has_permission("edit"))
93
            or (not page.botMayEdit())
94
        ):
95
            return False
×
96
        else:
97
            return True
×
98

99
    def get_old_archive_(self):
×
100
        return super().get_old_archive()
×
101

102
    def get_old_archive(self):
×
103
        # Archives will have already been reviewed by the archive_status_hook
104
        if self.status != "fail":
×
105
            return super().get_old_archive()
×
106
        return ""
×
107

108
    def archive_status_hook(self) -> None:
×
109
        if self._status == "fail":
×
110
            super().get_old_archive()
×
111
            if self.archive:
×
112
                print(
×
113
                    f"This file would fail because of the {self.ina_license} license, "
114
                    f"but an archived copy is available at {self.archive}."
115
                )
116
                new_license = click.prompt(
×
117
                    "Archive license (leave blank for no change)",
118
                    default=self.ina_license,
119
                )
120
                if new_license:
×
121
                    self.ina_license = new_license
×
122
                    del self.status
×
123
                    self.status
×
124

125
    def pre_save(self, new_text, summary, **kwargs):
×
126
        print(
×
127
            f"{self.page.title(as_link=True)} reviewed with status {self.status} "
128
            f"and license {self.ina_license}"
129
        )
130
        if self.status == "error":
×
131
            raise RuntimeError
×
132

133
        diff = difflib.unified_diff(
×
134
            self.page.get().split("\n"), new_text.split("\n"), lineterm=""
135
        )
136
        print("\n".join(diff))
×
137
        choice = click.confirm("Save the page?", default=True)
×
138
        if choice:
×
139
            return new_text, summary
×
140
        else:
141
            raise RuntimeError
×
142

143
    def id_hook(
×
144
        self,
145
        observations: Sequence[inrbot.iNaturalistID] = [],
146
        photos: Sequence[inrbot.iNaturalistID] = [],
147
        **kwargs,
148
    ):
149
        try:
×
150
            return ids[self.page]
×
151
        except KeyError:
×
152
            return self.ask_url(observations=observations, photos=photos)
×
153
        return None
154

155
    def ask_url(
×
156
        self,
157
        observations: Sequence[inrbot.iNaturalistID] = [],
158
        photos: Sequence[inrbot.iNaturalistID] = [],
159
    ):
160
        print(f"Commons page: {self.page.full_url()}")
×
161
        if observations:
×
162
            print(f"Observation ID found: {str(observations[0])}")
×
163
        if photos:
×
164
            print(f"Photo ID found: {str(photos[0])}")
×
165
        if auto_open:
×
166
            webbrowser.open(self.page.full_url())
×
167
            if observations and not photos:
×
168
                webbrowser.open(str(observations[0]))
×
169
            elif photos:
×
170
                webbrowser.open(str(photos[0]))
×
171

172
        res = click.prompt(
×
173
            "Is this ID correct? [Y/n/r/s/q]",
174
            default="Y",
175
            type=click.Choice("ynrsq", case_sensitive=False),
176
            show_default=False,
177
            show_choices=False,
178
        ).lower()
179
        if res == "y":
×
180
            correct_id = True
×
181
        elif res == "n":
×
182
            correct_id = False
×
183
        elif res == "r":
×
184
            self.remove_untagged_log()
×
185
            raise SkipFile
×
186
        elif res == "s":
×
187
            raise SkipFile
×
188
        elif res == "q":
×
189
            sys.exit()
×
190

191
        if not correct_id:
×
192
            url = click.prompt("iNaturalist Photos URL")
×
193
            ina_id = inrbot.parse_ina_url(url)
×
194
            if observations:
×
195
                self.page.text = self.page.text.replace(str(observations[0]), url)
×
196
            if photos:
×
197
                self.page.text = self.page.text.replace(str(photos[0]), url)
×
198
        elif observations and not photos:
×
199
            url = click.prompt("iNaturalist Photos URL", default="")
×
200
            ina_id = inrbot.parse_ina_url(url)
×
201
        elif photos:
×
202
            ina_id = photos[0]
×
203
        else:
204
            ina_id = None
×
205
        ids[self.page] = ina_id
×
206
        return ina_id
×
207

208
    def log_untagged_error(self) -> None:
×
209
        # Errors while running in CLI do not need to be logged on-wiki
210
        return
×
211

212

213
inrbot.id_hooks.append(ManualCommonsPage.id_hook)
×
214
inrbot.lock_hooks.append(ManualCommonsPage.archive_status_hook)
×
215
inrbot.pre_save_hooks.append(ManualCommonsPage.pre_save)
×
216

217

218
@click.command()
×
219
@click.argument("target")
×
220
@click.option("--url")
×
221
@click.option("--simulate/--no-simulate")
×
222
@click.option("--reverse", is_flag=True, default=False)
×
223
@click.option("-o", "--auto-open", "auto_open_", is_flag=True, default=False)
×
224
def main(target, url="", simulate=False, reverse=False, auto_open_=False):
×
225
    inrbot.simulate = simulate
×
226
    global auto_open
227
    auto_open = auto_open_
×
228
    if target == "auto":
×
229
        cat = pywikibot.Category(
×
230
            site, "Category:iNaturalist images needing human review"
231
        )
232
        dtt = pywikibot.Page(site, "Template:Deletion template tag")
×
233
        for page in cat.articles(namespaces=6, reverse=reverse):
×
234
            if dtt in set(page.itertemplates()):
×
235
                continue
×
236
            try:
×
237
                mcp = ManualCommonsPage(pywikibot.FilePage(page))
×
238
                mcp.review_file()
×
239
            except SkipFile:
×
240
                continue
×
241
    elif target == "errors":
×
242
        log_page = pywikibot.Page(site, inrbot.config["untagged_log_page"])
×
243
        dtt = pywikibot.Page(site, "Template:Deletion template tag")
×
244
        for page in log_page.linkedPages(namespaces=6, follow_redirects=True):
×
245
            mcp = ManualCommonsPage(pywikibot.FilePage(page))
×
246
            if (
×
247
                not page.exists()
248
                or dtt in set(page.itertemplates())
249
                or mcp.check_has_template()
250
            ):
251
                mcp.remove_untagged_log()
×
252
                continue
×
253
            try:
×
254
                mcp.review_file()
×
255
            except SkipFile:
×
256
                continue
×
257
    elif target == "ask":
×
258
        while True:
×
259
            new_target = click.prompt("Target", default="")
×
260
            if not new_target:
×
261
                break
×
262
            ManualCommonsPage(pywikibot.FilePage(site, new_target)).review_file()
×
263
    else:
264
        page = pywikibot.FilePage(site, target)
×
265
        if url:
×
266
            # TODO: Add validation
267
            ids[page] = inrbot.parse_ina_url(url)
×
268

269
        ManualCommonsPage(page).review_file()
×
270

271

272
if __name__ == "__main__":
×
273
    main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc