summaryrefslogtreecommitdiffstats
path: root/python/fatcat_tools/importers/arabesque.py
blob: b4a4d9ed14761dd020c5d90e48291b2deb8e7539 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
from typing import Any, Dict, List, Optional

import fatcat_openapi_client
from fatcat_openapi_client import ApiClient, FileEntity

from .common import SANE_MAX_RELEASES, SANE_MAX_URLS, EntityImporter, b32_hex, make_rel_url

ARABESQUE_MATCH_WHERE_CLAUSE = "WHERE hit = 1 AND identifier IS NOT NULL"


class ArabesqueMatchImporter(EntityImporter):
    """
    Importer for arabesque crawl report .sqlite files, which contain
    file/identifier matches based on URL/identifier seedlists.

    Uses a SQL query to iterate through table looking for rows with:

    - GROBID status 200
    - known SHA-1 (base32 format)
    - known timestamp

    Looks up release (by identifier) and file (by SHA-1). If no release exists,
    bail.

    If no file exists, create one from metadata (using both direct and wayback
    URLs), link to release, and insert.

    If file exists, optionally update it:

    - if no release match, match to the release
    - if new URL not included, add it (and wayback)

    Config options:
    - default URL rel
    - crawl id (for editgroup metadata)
    - identifier type

    TODO:
    - a mode to insert bare files even if identifier not known?
    """

    def __init__(
        self, api: ApiClient, extid_type: str, require_grobid: bool = True, **kwargs
    ) -> None:

        eg_desc = (
            kwargs.get("editgroup_description", None)
            or "Match web crawl files to releases based on identifier/URL seedlist"
        )
        eg_extra = kwargs.get("editgroup_extra", dict())
        eg_extra["agent"] = eg_extra.get("agent", "fatcat_tools.ArabesqueMatchImporter")
        if kwargs.get("crawl_id"):
            eg_extra["crawl_id"] = kwargs.get("crawl_id")
        kwargs["do_updates"] = kwargs.get("do_updates", False)
        super().__init__(api, editgroup_description=eg_desc, editgroup_extra=eg_extra, **kwargs)
        assert extid_type in ("doi", "pmcid", "pmid")
        self.extid_type = extid_type
        self.default_link_rel = kwargs.get("default_link_rel", "web")
        assert self.default_link_rel
        self.default_mimetype = kwargs.get("default_mimetype", None)
        self.require_grobid = require_grobid
        if self.require_grobid:
            print("Requiring GROBID status == 200")
        else:
            print("NOT checking GROBID status column")

    def want(self, row: Any) -> bool:
        if self.require_grobid and not row["postproc_status"] == "200":
            return False
        if (
            bool(row["hit"]) is True
            and row["final_sha1"]
            and row["final_timestamp"]
            and row["final_timestamp"] != "-"
            and len(row["final_timestamp"]) == 14
            and row["final_mimetype"]
            and bool(row["hit"]) is True
            and row["identifier"]
        ):
            return True
        else:
            return False

    def parse_record(self, row: Dict[str, Any]) -> Optional[FileEntity]:

        extid = row["identifier"].strip()

        # check/cleanup DOI
        if self.extid_type == "doi":
            extid = extid.lower()
            extid.replace("http://doi.org/", "")
            extid.replace("https://doi.org/", "")
            if extid.startswith("doi:"):
                extid = extid[4:]
            if not extid.startswith("10."):
                self.counts["skip-extid-invalid"]
                return None

        # lookup extid
        try:
            re = self.api.lookup_release(**{self.extid_type: extid})
        except fatcat_openapi_client.rest.ApiException as err:
            if err.status == 404:
                # bail on 404 (release not in DB)
                self.counts["skip-extid-not-found"] += 1
                return None
            elif err.status == 400:
                self.counts["skip-extid-invalid"] += 1
                return None
            else:
                raise err

        url = make_rel_url(row["final_url"], self.default_link_rel)
        if not url:
            self.counts["skip-url"] += 1
            return None
        if not row["final_timestamp"]:
            self.counts["skip-missing-timestamp"] += 1
            return None
        wayback = "https://web.archive.org/web/{}/{}".format(
            row["final_timestamp"], row["final_url"]
        )
        urls = [url, ("webarchive", wayback)]

        urls = [fatcat_openapi_client.FileUrl(rel=rel, url=url) for (rel, url) in urls]

        if len(urls) > SANE_MAX_URLS:
            self.counts["skip-too-many-url"] += 1
            return None

        fe = fatcat_openapi_client.FileEntity(
            sha1=b32_hex(row["final_sha1"]),
            mimetype=row["final_mimetype"] or self.default_mimetype,
            release_ids=[re.ident],
            urls=urls,
        )
        return fe

    def try_update(self, fe: FileEntity) -> bool:
        # lookup sha1, or create new entity
        existing = None
        try:
            existing = self.api.lookup_file(sha1=fe.sha1)
        except fatcat_openapi_client.rest.ApiException as err:
            if err.status != 404:
                raise err

        if not existing:
            return True

        if (fe.release_ids[0] in existing.release_ids) and existing.urls:
            # TODO: could still, in theory update with the new URL?
            self.counts["exists"] += 1
            return False

        if not self.do_updates:
            self.counts["skip-update-disabled"] += 1
            return False

        if existing.ident in [e.ident for e in self._edits_inflight]:
            self.counts["skip-update-inflight"] += 1
            return False

        # TODO: this code path never gets hit because of the check above
        if set(fe.release_ids) == set(existing.release_ids):
            existing_urls = set([u.url for u in existing.urls])
            new_urls = set([u.url for u in fe.urls])
            if existing_urls.issuperset(new_urls):
                self.counts["skip-update-nothing-new"] += 1
                return False

        # merge the existing into this one and update
        existing.urls = list(set([(u.rel, u.url) for u in fe.urls + existing.urls]))
        existing.urls = [
            fatcat_openapi_client.FileUrl(rel=rel, url=url) for (rel, url) in existing.urls
        ]
        if len(existing.urls) > SANE_MAX_URLS:
            self.counts["skip-update-too-many-url"] += 1
            return False
        existing.release_ids = list(set(fe.release_ids + existing.release_ids))
        if len(existing.release_ids) > SANE_MAX_RELEASES:
            self.counts["skip-update-too-many-url"] += 1
            return False
        existing.mimetype = existing.mimetype or fe.mimetype
        edit = self.api.update_file(self.get_editgroup_id(), existing.ident, existing)
        self._edits_inflight.append(edit)
        self.counts["update"] += 1
        return False

    def insert_batch(self, batch: List[FileEntity]) -> None:
        self.api.create_file_auto_batch(
            fatcat_openapi_client.FileAutoBatch(
                editgroup=fatcat_openapi_client.Editgroup(
                    description=self.editgroup_description, extra=self.editgroup_extra
                ),
                entity_list=batch,
            )
        )