We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Expand book data task is re-setting default editions
it should not.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bookwyrm/connectors/abstract_connector.py`
Content:
```
1 ''' functionality outline for a book data connector '''
2 from abc import ABC, abstractmethod
3 from dataclasses import asdict, dataclass
4 import logging
5 from urllib3.exceptions import RequestError
6 
7 from django.db import transaction
8 import requests
9 from requests.exceptions import SSLError
10 
11 from bookwyrm import activitypub, models, settings
12 from .connector_manager import load_more_data, ConnectorException
13 
14 
15 logger = logging.getLogger(__name__)
16 class AbstractMinimalConnector(ABC):
17     ''' just the bare bones, for other bookwyrm instances '''
18     def __init__(self, identifier):
19         # load connector settings
20         info = models.Connector.objects.get(identifier=identifier)
21         self.connector = info
22 
23         # the things in the connector model to copy over
24         self_fields = [
25             'base_url',
26             'books_url',
27             'covers_url',
28             'search_url',
29             'max_query_count',
30             'name',
31             'identifier',
32             'local'
33         ]
34         for field in self_fields:
35             setattr(self, field, getattr(info, field))
36 
37     def search(self, query, min_confidence=None):
38         ''' free text search '''
39         params = {}
40         if min_confidence:
41             params['min_confidence'] = min_confidence
42 
43         resp = requests.get(
44             '%s%s' % (self.search_url, query),
45             params=params,
46             headers={
47                 'Accept': 'application/json; charset=utf-8',
48                 'User-Agent': settings.USER_AGENT,
49             },
50         )
51         if not resp.ok:
52             resp.raise_for_status()
53         try:
54             data = resp.json()
55         except ValueError as e:
56             logger.exception(e)
57             raise ConnectorException('Unable to parse json response', e)
58         results = []
59 
60         for doc in self.parse_search_data(data)[:10]:
61             results.append(self.format_search_result(doc))
62         return results
63 
64     @abstractmethod
65     def get_or_create_book(self, remote_id):
66         ''' pull up a book record by whatever means possible '''
67 
68     @abstractmethod
69     def parse_search_data(self, data):
70         ''' turn the result json from a search into a list '''
71 
72     @abstractmethod
73     def format_search_result(self, search_result):
74         ''' create a SearchResult obj from json '''
75 
76 
77 class AbstractConnector(AbstractMinimalConnector):
78     ''' generic book data connector '''
79     def __init__(self, identifier):
80         super().__init__(identifier)
81         # fields we want to look for in book data to copy over
82         # title we handle separately.
83         self.book_mappings = []
84 
85 
86     def is_available(self):
87         ''' check if you're allowed to use this connector '''
88         if self.max_query_count is not None:
89             if self.connector.query_count >= self.max_query_count:
90                 return False
91         return True
92 
93 
94     def get_or_create_book(self, remote_id):
95         ''' translate arbitrary json into an Activitypub dataclass '''
96         # first, check if we have the origin_id saved
97         existing = models.Edition.find_existing_by_remote_id(remote_id) or \
98                 models.Work.find_existing_by_remote_id(remote_id)
99         if existing:
100             if hasattr(existing, 'get_default_editon'):
101                 return existing.get_default_editon()
102             return existing
103 
104         # load the json
105         data = get_data(remote_id)
106         mapped_data = dict_from_mappings(data, self.book_mappings)
107         if self.is_work_data(data):
108             try:
109                 edition_data = self.get_edition_from_work_data(data)
110             except (KeyError, ConnectorException):
111                 # hack: re-use the work data as the edition data
112                 # this is why remote ids aren't necessarily unique
113                 edition_data = data
114             work_data = mapped_data
115         else:
116             try:
117                 work_data = self.get_work_from_edition_data(data)
118                 work_data = dict_from_mappings(work_data, self.book_mappings)
119             except (KeyError, ConnectorException):
120                 work_data = mapped_data
121             edition_data = data
122 
123         if not work_data or not edition_data:
124             raise ConnectorException('Unable to load book data: %s' % remote_id)
125 
126         with transaction.atomic():
127             # create activitypub object
128             work_activity = activitypub.Work(**work_data)
129             # this will dedupe automatically
130             work = work_activity.to_model(models.Work)
131             for author in self.get_authors_from_data(data):
132                 work.authors.add(author)
133 
134             edition = self.create_edition_from_data(work, edition_data)
135         load_more_data.delay(self.connector.id, work.id)
136         return edition
137 
138 
139     def create_edition_from_data(self, work, edition_data):
140         ''' if we already have the work, we're ready '''
141         mapped_data = dict_from_mappings(edition_data, self.book_mappings)
142         mapped_data['work'] = work.remote_id
143         edition_activity = activitypub.Edition(**mapped_data)
144         edition = edition_activity.to_model(models.Edition)
145         edition.connector = self.connector
146         edition.save()
147 
148         work.default_edition = edition
149         work.save()
150 
151         for author in self.get_authors_from_data(edition_data):
152             edition.authors.add(author)
153         if not edition.authors.exists() and work.authors.exists():
154             edition.authors.set(work.authors.all())
155 
156         return edition
157 
158 
159     def get_or_create_author(self, remote_id):
160         ''' load that author '''
161         existing = models.Author.find_existing_by_remote_id(remote_id)
162         if existing:
163             return existing
164 
165         data = get_data(remote_id)
166 
167         mapped_data = dict_from_mappings(data, self.author_mappings)
168         activity = activitypub.Author(**mapped_data)
169         # this will dedupe
170         return activity.to_model(models.Author)
171 
172 
173     @abstractmethod
174     def is_work_data(self, data):
175         ''' differentiate works and editions '''
176 
177     @abstractmethod
178     def get_edition_from_work_data(self, data):
179         ''' every work needs at least one edition '''
180 
181     @abstractmethod
182     def get_work_from_edition_data(self, data):
183         ''' every edition needs a work '''
184 
185     @abstractmethod
186     def get_authors_from_data(self, data):
187         ''' load author data '''
188 
189     @abstractmethod
190     def expand_book_data(self, book):
191         ''' get more info on a book '''
192 
193 
194 def dict_from_mappings(data, mappings):
195     ''' create a dict in Activitypub format, using mappings supplies by
196     the subclass '''
197     result = {}
198     for mapping in mappings:
199         result[mapping.local_field] = mapping.get_value(data)
200     return result
201 
202 
203 def get_data(url):
204     ''' wrapper for request.get '''
205     try:
206         resp = requests.get(
207             url,
208             headers={
209                 'Accept': 'application/json; charset=utf-8',
210                 'User-Agent': settings.USER_AGENT,
211             },
212         )
213     except (RequestError, SSLError) as e:
214         logger.exception(e)
215         raise ConnectorException()
216 
217     if not resp.ok:
218         try:
219             resp.raise_for_status()
220         except requests.exceptions.HTTPError as e:
221             logger.exception(e)
222             raise ConnectorException()
223     try:
224         data = resp.json()
225     except ValueError as e:
226         logger.exception(e)
227         raise ConnectorException()
228 
229     return data
230 
231 
232 def get_image(url):
233     ''' wrapper for requesting an image '''
234     try:
235         resp = requests.get(
236             url,
237             headers={
238                 'User-Agent': settings.USER_AGENT,
239             },
240         )
241     except (RequestError, SSLError):
242         return None
243     if not resp.ok:
244         return None
245     return resp
246 
247 
248 @dataclass
249 class SearchResult:
250     ''' standardized search result object '''
251     title: str
252     key: str
253     author: str
254     year: str
255     connector: object
256     confidence: int = 1
257 
258     def __repr__(self):
259         return "<SearchResult key={!r} title={!r} author={!r}>".format(
260             self.key, self.title, self.author)
261 
262     def json(self):
263         ''' serialize a connector for json response '''
264         serialized = asdict(self)
265         del serialized['connector']
266         return serialized
267 
268 
269 class Mapping:
270     ''' associate a local database field with a field in an external dataset '''
271     def __init__(self, local_field, remote_field=None, formatter=None):
272         noop = lambda x: x
273 
274         self.local_field = local_field
275         self.remote_field = remote_field or local_field
276         self.formatter = formatter or noop
277 
278     def get_value(self, data):
279         ''' pull a field from incoming json and return the formatted version '''
280         value = data.get(self.remote_field)
281         if not value:
282             return None
283         try:
284             return self.formatter(value)
285         except:# pylint: disable=bare-except
286             return None
287
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py
--- a/bookwyrm/connectors/abstract_connector.py
+++ b/bookwyrm/connectors/abstract_connector.py
@@ -145,8 +145,9 @@
         edition.connector = self.connector
         edition.save()
 
-        work.default_edition = edition
-        work.save()
+        if not work.default_edition:
+            work.default_edition = edition
+            work.save()
 
         for author in self.get_authors_from_data(edition_data):
             edition.authors.add(author)

{"golden_diff": "diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py\n--- a/bookwyrm/connectors/abstract_connector.py\n+++ b/bookwyrm/connectors/abstract_connector.py\n@@ -145,8 +145,9 @@\n         edition.connector = self.connector\n         edition.save()\n \n-        work.default_edition = edition\n-        work.save()\n+        if not work.default_edition:\n+            work.default_edition = edition\n+            work.save()\n \n         for author in self.get_authors_from_data(edition_data):\n             edition.authors.add(author)\n", "issue": "Expand book data task is re-setting default editions\nit should not.\n", "before_files": [{"content": "''' functionality outline for a book data connector '''\nfrom abc import ABC, abstractmethod\nfrom dataclasses import asdict, dataclass\nimport logging\nfrom urllib3.exceptions import RequestError\n\nfrom django.db import transaction\nimport requests\nfrom requests.exceptions import SSLError\n\nfrom bookwyrm import activitypub, models, settings\nfrom .connector_manager import load_more_data, ConnectorException\n\n\nlogger = logging.getLogger(__name__)\nclass AbstractMinimalConnector(ABC):\n    ''' just the bare bones, for other bookwyrm instances '''\n    def __init__(self, identifier):\n        # load connector settings\n        info = models.Connector.objects.get(identifier=identifier)\n        self.connector = info\n\n        # the things in the connector model to copy over\n        self_fields = [\n            'base_url',\n            'books_url',\n            'covers_url',\n            'search_url',\n            'max_query_count',\n            'name',\n            'identifier',\n            'local'\n        ]\n        for field in self_fields:\n            setattr(self, field, getattr(info, field))\n\n    def search(self, query, min_confidence=None):\n        ''' free text search '''\n        params = {}\n        if min_confidence:\n            params['min_confidence'] = min_confidence\n\n        resp = requests.get(\n            '%s%s' % (self.search_url, query),\n            params=params,\n            headers={\n                'Accept': 'application/json; charset=utf-8',\n                'User-Agent': settings.USER_AGENT,\n            },\n        )\n        if not resp.ok:\n            resp.raise_for_status()\n        try:\n            data = resp.json()\n        except ValueError as e:\n            logger.exception(e)\n            raise ConnectorException('Unable to parse json response', e)\n        results = []\n\n        for doc in self.parse_search_data(data)[:10]:\n            results.append(self.format_search_result(doc))\n        return results\n\n    @abstractmethod\n    def get_or_create_book(self, remote_id):\n        ''' pull up a book record by whatever means possible '''\n\n    @abstractmethod\n    def parse_search_data(self, data):\n        ''' turn the result json from a search into a list '''\n\n    @abstractmethod\n    def format_search_result(self, search_result):\n        ''' create a SearchResult obj from json '''\n\n\nclass AbstractConnector(AbstractMinimalConnector):\n    ''' generic book data connector '''\n    def __init__(self, identifier):\n        super().__init__(identifier)\n        # fields we want to look for in book data to copy over\n        # title we handle separately.\n        self.book_mappings = []\n\n\n    def is_available(self):\n        ''' check if you're allowed to use this connector '''\n        if self.max_query_count is not None:\n            if self.connector.query_count >= self.max_query_count:\n                return False\n        return True\n\n\n    def get_or_create_book(self, remote_id):\n        ''' translate arbitrary json into an Activitypub dataclass '''\n        # first, check if we have the origin_id saved\n        existing = models.Edition.find_existing_by_remote_id(remote_id) or \\\n                models.Work.find_existing_by_remote_id(remote_id)\n        if existing:\n            if hasattr(existing, 'get_default_editon'):\n                return existing.get_default_editon()\n            return existing\n\n        # load the json\n        data = get_data(remote_id)\n        mapped_data = dict_from_mappings(data, self.book_mappings)\n        if self.is_work_data(data):\n            try:\n                edition_data = self.get_edition_from_work_data(data)\n            except (KeyError, ConnectorException):\n                # hack: re-use the work data as the edition data\n                # this is why remote ids aren't necessarily unique\n                edition_data = data\n            work_data = mapped_data\n        else:\n            try:\n                work_data = self.get_work_from_edition_data(data)\n                work_data = dict_from_mappings(work_data, self.book_mappings)\n            except (KeyError, ConnectorException):\n                work_data = mapped_data\n            edition_data = data\n\n        if not work_data or not edition_data:\n            raise ConnectorException('Unable to load book data: %s' % remote_id)\n\n        with transaction.atomic():\n            # create activitypub object\n            work_activity = activitypub.Work(**work_data)\n            # this will dedupe automatically\n            work = work_activity.to_model(models.Work)\n            for author in self.get_authors_from_data(data):\n                work.authors.add(author)\n\n            edition = self.create_edition_from_data(work, edition_data)\n        load_more_data.delay(self.connector.id, work.id)\n        return edition\n\n\n    def create_edition_from_data(self, work, edition_data):\n        ''' if we already have the work, we're ready '''\n        mapped_data = dict_from_mappings(edition_data, self.book_mappings)\n        mapped_data['work'] = work.remote_id\n        edition_activity = activitypub.Edition(**mapped_data)\n        edition = edition_activity.to_model(models.Edition)\n        edition.connector = self.connector\n        edition.save()\n\n        work.default_edition = edition\n        work.save()\n\n        for author in self.get_authors_from_data(edition_data):\n            edition.authors.add(author)\n        if not edition.authors.exists() and work.authors.exists():\n            edition.authors.set(work.authors.all())\n\n        return edition\n\n\n    def get_or_create_author(self, remote_id):\n        ''' load that author '''\n        existing = models.Author.find_existing_by_remote_id(remote_id)\n        if existing:\n            return existing\n\n        data = get_data(remote_id)\n\n        mapped_data = dict_from_mappings(data, self.author_mappings)\n        activity = activitypub.Author(**mapped_data)\n        # this will dedupe\n        return activity.to_model(models.Author)\n\n\n    @abstractmethod\n    def is_work_data(self, data):\n        ''' differentiate works and editions '''\n\n    @abstractmethod\n    def get_edition_from_work_data(self, data):\n        ''' every work needs at least one edition '''\n\n    @abstractmethod\n    def get_work_from_edition_data(self, data):\n        ''' every edition needs a work '''\n\n    @abstractmethod\n    def get_authors_from_data(self, data):\n        ''' load author data '''\n\n    @abstractmethod\n    def expand_book_data(self, book):\n        ''' get more info on a book '''\n\n\ndef dict_from_mappings(data, mappings):\n    ''' create a dict in Activitypub format, using mappings supplies by\n    the subclass '''\n    result = {}\n    for mapping in mappings:\n        result[mapping.local_field] = mapping.get_value(data)\n    return result\n\n\ndef get_data(url):\n    ''' wrapper for request.get '''\n    try:\n        resp = requests.get(\n            url,\n            headers={\n                'Accept': 'application/json; charset=utf-8',\n                'User-Agent': settings.USER_AGENT,\n            },\n        )\n    except (RequestError, SSLError) as e:\n        logger.exception(e)\n        raise ConnectorException()\n\n    if not resp.ok:\n        try:\n            resp.raise_for_status()\n        except requests.exceptions.HTTPError as e:\n            logger.exception(e)\n            raise ConnectorException()\n    try:\n        data = resp.json()\n    except ValueError as e:\n        logger.exception(e)\n        raise ConnectorException()\n\n    return data\n\n\ndef get_image(url):\n    ''' wrapper for requesting an image '''\n    try:\n        resp = requests.get(\n            url,\n            headers={\n                'User-Agent': settings.USER_AGENT,\n            },\n        )\n    except (RequestError, SSLError):\n        return None\n    if not resp.ok:\n        return None\n    return resp\n\n\n@dataclass\nclass SearchResult:\n    ''' standardized search result object '''\n    title: str\n    key: str\n    author: str\n    year: str\n    connector: object\n    confidence: int = 1\n\n    def __repr__(self):\n        return \"<SearchResult key={!r} title={!r} author={!r}>\".format(\n            self.key, self.title, self.author)\n\n    def json(self):\n        ''' serialize a connector for json response '''\n        serialized = asdict(self)\n        del serialized['connector']\n        return serialized\n\n\nclass Mapping:\n    ''' associate a local database field with a field in an external dataset '''\n    def __init__(self, local_field, remote_field=None, formatter=None):\n        noop = lambda x: x\n\n        self.local_field = local_field\n        self.remote_field = remote_field or local_field\n        self.formatter = formatter or noop\n\n    def get_value(self, data):\n        ''' pull a field from incoming json and return the formatted version '''\n        value = data.get(self.remote_field)\n        if not value:\n            return None\n        try:\n            return self.formatter(value)\n        except:# pylint: disable=bare-except\n            return None\n", "path": "bookwyrm/connectors/abstract_connector.py"}], "after_files": [{"content": "''' functionality outline for a book data connector '''\nfrom abc import ABC, abstractmethod\nfrom dataclasses import asdict, dataclass\nimport logging\nfrom urllib3.exceptions import RequestError\n\nfrom django.db import transaction\nimport requests\nfrom requests.exceptions import SSLError\n\nfrom bookwyrm import activitypub, models, settings\nfrom .connector_manager import load_more_data, ConnectorException\n\n\nlogger = logging.getLogger(__name__)\nclass AbstractMinimalConnector(ABC):\n    ''' just the bare bones, for other bookwyrm instances '''\n    def __init__(self, identifier):\n        # load connector settings\n        info = models.Connector.objects.get(identifier=identifier)\n        self.connector = info\n\n        # the things in the connector model to copy over\n        self_fields = [\n            'base_url',\n            'books_url',\n            'covers_url',\n            'search_url',\n            'max_query_count',\n            'name',\n            'identifier',\n            'local'\n        ]\n        for field in self_fields:\n            setattr(self, field, getattr(info, field))\n\n    def search(self, query, min_confidence=None):\n        ''' free text search '''\n        params = {}\n        if min_confidence:\n            params['min_confidence'] = min_confidence\n\n        resp = requests.get(\n            '%s%s' % (self.search_url, query),\n            params=params,\n            headers={\n                'Accept': 'application/json; charset=utf-8',\n                'User-Agent': settings.USER_AGENT,\n            },\n        )\n        if not resp.ok:\n            resp.raise_for_status()\n        try:\n            data = resp.json()\n        except ValueError as e:\n            logger.exception(e)\n            raise ConnectorException('Unable to parse json response', e)\n        results = []\n\n        for doc in self.parse_search_data(data)[:10]:\n            results.append(self.format_search_result(doc))\n        return results\n\n    @abstractmethod\n    def get_or_create_book(self, remote_id):\n        ''' pull up a book record by whatever means possible '''\n\n    @abstractmethod\n    def parse_search_data(self, data):\n        ''' turn the result json from a search into a list '''\n\n    @abstractmethod\n    def format_search_result(self, search_result):\n        ''' create a SearchResult obj from json '''\n\n\nclass AbstractConnector(AbstractMinimalConnector):\n    ''' generic book data connector '''\n    def __init__(self, identifier):\n        super().__init__(identifier)\n        # fields we want to look for in book data to copy over\n        # title we handle separately.\n        self.book_mappings = []\n\n\n    def is_available(self):\n        ''' check if you're allowed to use this connector '''\n        if self.max_query_count is not None:\n            if self.connector.query_count >= self.max_query_count:\n                return False\n        return True\n\n\n    def get_or_create_book(self, remote_id):\n        ''' translate arbitrary json into an Activitypub dataclass '''\n        # first, check if we have the origin_id saved\n        existing = models.Edition.find_existing_by_remote_id(remote_id) or \\\n                models.Work.find_existing_by_remote_id(remote_id)\n        if existing:\n            if hasattr(existing, 'get_default_editon'):\n                return existing.get_default_editon()\n            return existing\n\n        # load the json\n        data = get_data(remote_id)\n        mapped_data = dict_from_mappings(data, self.book_mappings)\n        if self.is_work_data(data):\n            try:\n                edition_data = self.get_edition_from_work_data(data)\n            except (KeyError, ConnectorException):\n                # hack: re-use the work data as the edition data\n                # this is why remote ids aren't necessarily unique\n                edition_data = data\n            work_data = mapped_data\n        else:\n            try:\n                work_data = self.get_work_from_edition_data(data)\n                work_data = dict_from_mappings(work_data, self.book_mappings)\n            except (KeyError, ConnectorException):\n                work_data = mapped_data\n            edition_data = data\n\n        if not work_data or not edition_data:\n            raise ConnectorException('Unable to load book data: %s' % remote_id)\n\n        with transaction.atomic():\n            # create activitypub object\n            work_activity = activitypub.Work(**work_data)\n            # this will dedupe automatically\n            work = work_activity.to_model(models.Work)\n            for author in self.get_authors_from_data(data):\n                work.authors.add(author)\n\n            edition = self.create_edition_from_data(work, edition_data)\n        load_more_data.delay(self.connector.id, work.id)\n        return edition\n\n\n    def create_edition_from_data(self, work, edition_data):\n        ''' if we already have the work, we're ready '''\n        mapped_data = dict_from_mappings(edition_data, self.book_mappings)\n        mapped_data['work'] = work.remote_id\n        edition_activity = activitypub.Edition(**mapped_data)\n        edition = edition_activity.to_model(models.Edition)\n        edition.connector = self.connector\n        edition.save()\n\n        if not work.default_edition:\n            work.default_edition = edition\n            work.save()\n\n        for author in self.get_authors_from_data(edition_data):\n            edition.authors.add(author)\n        if not edition.authors.exists() and work.authors.exists():\n            edition.authors.set(work.authors.all())\n\n        return edition\n\n\n    def get_or_create_author(self, remote_id):\n        ''' load that author '''\n        existing = models.Author.find_existing_by_remote_id(remote_id)\n        if existing:\n            return existing\n\n        data = get_data(remote_id)\n\n        mapped_data = dict_from_mappings(data, self.author_mappings)\n        activity = activitypub.Author(**mapped_data)\n        # this will dedupe\n        return activity.to_model(models.Author)\n\n\n    @abstractmethod\n    def is_work_data(self, data):\n        ''' differentiate works and editions '''\n\n    @abstractmethod\n    def get_edition_from_work_data(self, data):\n        ''' every work needs at least one edition '''\n\n    @abstractmethod\n    def get_work_from_edition_data(self, data):\n        ''' every edition needs a work '''\n\n    @abstractmethod\n    def get_authors_from_data(self, data):\n        ''' load author data '''\n\n    @abstractmethod\n    def expand_book_data(self, book):\n        ''' get more info on a book '''\n\n\ndef dict_from_mappings(data, mappings):\n    ''' create a dict in Activitypub format, using mappings supplies by\n    the subclass '''\n    result = {}\n    for mapping in mappings:\n        result[mapping.local_field] = mapping.get_value(data)\n    return result\n\n\ndef get_data(url):\n    ''' wrapper for request.get '''\n    try:\n        resp = requests.get(\n            url,\n            headers={\n                'Accept': 'application/json; charset=utf-8',\n                'User-Agent': settings.USER_AGENT,\n            },\n        )\n    except (RequestError, SSLError) as e:\n        logger.exception(e)\n        raise ConnectorException()\n\n    if not resp.ok:\n        try:\n            resp.raise_for_status()\n        except requests.exceptions.HTTPError as e:\n            logger.exception(e)\n            raise ConnectorException()\n    try:\n        data = resp.json()\n    except ValueError as e:\n        logger.exception(e)\n        raise ConnectorException()\n\n    return data\n\n\ndef get_image(url):\n    ''' wrapper for requesting an image '''\n    try:\n        resp = requests.get(\n            url,\n            headers={\n                'User-Agent': settings.USER_AGENT,\n            },\n        )\n    except (RequestError, SSLError):\n        return None\n    if not resp.ok:\n        return None\n    return resp\n\n\n@dataclass\nclass SearchResult:\n    ''' standardized search result object '''\n    title: str\n    key: str\n    author: str\n    year: str\n    connector: object\n    confidence: int = 1\n\n    def __repr__(self):\n        return \"<SearchResult key={!r} title={!r} author={!r}>\".format(\n            self.key, self.title, self.author)\n\n    def json(self):\n        ''' serialize a connector for json response '''\n        serialized = asdict(self)\n        del serialized['connector']\n        return serialized\n\n\nclass Mapping:\n    ''' associate a local database field with a field in an external dataset '''\n    def __init__(self, local_field, remote_field=None, formatter=None):\n        noop = lambda x: x\n\n        self.local_field = local_field\n        self.remote_field = remote_field or local_field\n        self.formatter = formatter or noop\n\n    def get_value(self, data):\n        ''' pull a field from incoming json and return the formatted version '''\n        value = data.get(self.remote_field)\n        if not value:\n            return None\n        try:\n            return self.formatter(value)\n        except:# pylint: disable=bare-except\n            return None\n", "path": "bookwyrm/connectors/abstract_connector.py"}]}