diff --git a/bookwyrm/connectors/__init__.py b/bookwyrm/connectors/__init__.py index f6a558f36..cfafd2868 100644 --- a/bookwyrm/connectors/__init__.py +++ b/bookwyrm/connectors/__init__.py @@ -1,4 +1,6 @@ ''' bring connectors into the namespace ''' from .settings import CONNECTORS -from .abstract_connector import ConnectorException, load_connector +from .abstract_connector import ConnectorException from .abstract_connector import get_data, get_image + +from .connector_manager import search, local_search, first_search_result diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index c1f5a3216..82b99378f 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -1,24 +1,18 @@ ''' functionality outline for a book data connector ''' from abc import ABC, abstractmethod from dataclasses import asdict, dataclass -import importlib import logging from urllib3.exceptions import RequestError from django.db import transaction import requests -from requests import HTTPError from requests.exceptions import SSLError from bookwyrm import activitypub, models, settings -from bookwyrm.tasks import app +from .connector_manager import load_more_data, ConnectorException logger = logging.getLogger(__name__) -class ConnectorException(HTTPError): - ''' when the connector can't do what was asked ''' - - class AbstractMinimalConnector(ABC): ''' just the bare bones, for other bookwyrm instances ''' def __init__(self, identifier): @@ -192,23 +186,6 @@ class AbstractConnector(AbstractMinimalConnector): ''' get more info on a book ''' -@app.task -def load_more_data(connector_id, book_id): - ''' background the work of getting all 10,000 editions of LoTR ''' - connector_info = models.Connector.objects.get(id=connector_id) - connector = load_connector(connector_info) - book = models.Book.objects.select_subclasses().get(id=book_id) - connector.expand_book_data(book) - - -def load_connector(connector_info): - ''' instantiate the connector class ''' - connector = importlib.import_module( - 'bookwyrm.connectors.%s' % connector_info.connector_file - ) - return connector.Connector(connector_info.identifier) - - def dict_from_mappings(data, mappings): ''' create a dict in Activitypub format, using mappings supplies by the subclass ''' diff --git a/bookwyrm/books_manager.py b/bookwyrm/connectors/connector_manager.py similarity index 76% rename from bookwyrm/books_manager.py rename to bookwyrm/connectors/connector_manager.py index 70386ba83..d3b01f7ae 100644 --- a/bookwyrm/books_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -1,41 +1,15 @@ -''' select and call a connector for whatever book task needs doing ''' +''' interface with whatever connectors the app has ''' +import importlib from urllib.parse import urlparse from requests import HTTPError from bookwyrm import models -from bookwyrm.connectors import ConnectorException, load_connector +from bookwyrm.tasks import app -def get_edition(book_id): - ''' look up a book in the db and return an edition ''' - book = models.Book.objects.select_subclasses().get(id=book_id) - if isinstance(book, models.Work): - book = book.default_edition - return book - - -def get_or_create_connector(remote_id): - ''' get the connector related to the author's server ''' - url = urlparse(remote_id) - identifier = url.netloc - if not identifier: - raise ValueError('Invalid remote id') - - try: - connector_info = models.Connector.objects.get(identifier=identifier) - except models.Connector.DoesNotExist: - connector_info = models.Connector.objects.create( - identifier=identifier, - connector_file='bookwyrm_connector', - base_url='https://%s' % identifier, - books_url='https://%s/book' % identifier, - covers_url='https://%s/images/covers' % identifier, - search_url='https://%s/search?q=' % identifier, - priority=2 - ) - - return load_connector(connector_info) +class ConnectorException(HTTPError): + ''' when the connector can't do what was asked ''' def search(query, min_confidence=0.1): @@ -80,3 +54,43 @@ def get_connectors(): ''' load all connectors ''' for info in models.Connector.objects.order_by('priority').all(): yield load_connector(info) + + +def get_or_create_connector(remote_id): + ''' get the connector related to the author's server ''' + url = urlparse(remote_id) + identifier = url.netloc + if not identifier: + raise ValueError('Invalid remote id') + + try: + connector_info = models.Connector.objects.get(identifier=identifier) + except models.Connector.DoesNotExist: + connector_info = models.Connector.objects.create( + identifier=identifier, + connector_file='bookwyrm_connector', + base_url='https://%s' % identifier, + books_url='https://%s/book' % identifier, + covers_url='https://%s/images/covers' % identifier, + search_url='https://%s/search?q=' % identifier, + priority=2 + ) + + return load_connector(connector_info) + + +@app.task +def load_more_data(connector_id, book_id): + ''' background the work of getting all 10,000 editions of LoTR ''' + connector_info = models.Connector.objects.get(id=connector_id) + connector = load_connector(connector_info) + book = models.Book.objects.select_subclasses().get(id=book_id) + connector.expand_book_data(book) + + +def load_connector(connector_info): + ''' instantiate the connector class ''' + connector = importlib.import_module( + 'bookwyrm.connectors.%s' % connector_info.connector_file + ) + return connector.Connector(connector_info.identifier) diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index a1155d2d5..a5303145f 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -3,7 +3,7 @@ import re from bookwyrm import models from .abstract_connector import AbstractConnector, SearchResult, Mapping -from .abstract_connector import ConnectorException, get_data +from .connector_manager import ConnectorException, get_data from .openlibrary_languages import languages diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index 576dd07d1..1ebe9b31b 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -6,7 +6,7 @@ from django.contrib.postgres.fields import JSONField from django.db import models from django.utils import timezone -from bookwyrm import books_manager +from bookwyrm.connectors import connector_manager from bookwyrm.models import ReadThrough, User, Book from .fields import PrivacyLevels @@ -71,7 +71,7 @@ class ImportItem(models.Model): def get_book_from_isbn(self): ''' search by isbn ''' - search_result = books_manager.first_search_result( + search_result = connector_manager.first_search_result( self.isbn, min_confidence=0.999 ) if search_result: @@ -86,7 +86,7 @@ class ImportItem(models.Model): self.data['Title'], self.data['Author'] ) - search_result = books_manager.first_search_result( + search_result = connector_manager.first_search_result( search_term, min_confidence=0.999 ) if search_result: diff --git a/bookwyrm/tests/test_books_manager.py b/bookwyrm/tests/connectors/test_connector_manager.py similarity index 73% rename from bookwyrm/tests/test_books_manager.py rename to bookwyrm/tests/connectors/test_connector_manager.py index 039bdfc5b..0d5d952c7 100644 --- a/bookwyrm/tests/test_books_manager.py +++ b/bookwyrm/tests/connectors/test_connector_manager.py @@ -1,11 +1,12 @@ from django.test import TestCase -from bookwyrm import books_manager, models +from bookwyrm import models +from bookwyrm.connectors import connector_manager from bookwyrm.connectors.bookwyrm_connector import Connector as BookWyrmConnector from bookwyrm.connectors.self_connector import Connector as SelfConnector -class Book(TestCase): +class ConnectorManager(TestCase): def setUp(self): self.work = models.Work.objects.create( title='Example Work' @@ -29,52 +30,52 @@ class Book(TestCase): ) def test_get_edition(self): - edition = books_manager.get_edition(self.edition.id) + edition = connector_manager.get_edition(self.edition.id) self.assertEqual(edition, self.edition) def test_get_edition_work(self): - edition = books_manager.get_edition(self.work.id) + edition = connector_manager.get_edition(self.work.id) self.assertEqual(edition, self.edition) def test_get_or_create_connector(self): remote_id = 'https://example.com/object/1' - connector = books_manager.get_or_create_connector(remote_id) + connector = connector_manager.get_or_create_connector(remote_id) self.assertIsInstance(connector, BookWyrmConnector) self.assertEqual(connector.identifier, 'example.com') self.assertEqual(connector.base_url, 'https://example.com') - same_connector = books_manager.get_or_create_connector(remote_id) + same_connector = connector_manager.get_or_create_connector(remote_id) self.assertEqual(connector.identifier, same_connector.identifier) def test_get_connectors(self): remote_id = 'https://example.com/object/1' - books_manager.get_or_create_connector(remote_id) - connectors = list(books_manager.get_connectors()) + connector_manager.get_or_create_connector(remote_id) + connectors = list(connector_manager.get_connectors()) self.assertEqual(len(connectors), 2) self.assertIsInstance(connectors[0], SelfConnector) self.assertIsInstance(connectors[1], BookWyrmConnector) def test_search(self): - results = books_manager.search('Example') + results = connector_manager.search('Example') self.assertEqual(len(results), 1) self.assertIsInstance(results[0]['connector'], SelfConnector) self.assertEqual(len(results[0]['results']), 1) self.assertEqual(results[0]['results'][0].title, 'Example Edition') def test_local_search(self): - results = books_manager.local_search('Example') + results = connector_manager.local_search('Example') self.assertEqual(len(results), 1) self.assertEqual(results[0].title, 'Example Edition') def test_first_search_result(self): - result = books_manager.first_search_result('Example') + result = connector_manager.first_search_result('Example') self.assertEqual(result.title, 'Example Edition') - no_result = books_manager.first_search_result('dkjfhg') + no_result = connector_manager.first_search_result('dkjfhg') self.assertIsNone(no_result) def test_load_connector(self): - connector = books_manager.load_connector(self.connector) + connector = connector_manager.load_connector(self.connector) self.assertIsInstance(connector, SelfConnector) self.assertEqual(connector.identifier, 'test_connector') diff --git a/bookwyrm/tests/connectors/test_openlibrary_connector.py b/bookwyrm/tests/connectors/test_openlibrary_connector.py index 437b23dc5..10e747707 100644 --- a/bookwyrm/tests/connectors/test_openlibrary_connector.py +++ b/bookwyrm/tests/connectors/test_openlibrary_connector.py @@ -12,7 +12,7 @@ from bookwyrm.connectors.openlibrary import get_languages, get_description from bookwyrm.connectors.openlibrary import pick_default_edition, \ get_openlibrary_key from bookwyrm.connectors.abstract_connector import SearchResult -from bookwyrm.connectors.abstract_connector import ConnectorException +from bookwyrm.connectors.connector_manager import ConnectorException class Openlibrary(TestCase): diff --git a/bookwyrm/tests/models/test_import_model.py b/bookwyrm/tests/models/test_import_model.py index a975f410d..0c49bbe4e 100644 --- a/bookwyrm/tests/models/test_import_model.py +++ b/bookwyrm/tests/models/test_import_model.py @@ -8,7 +8,8 @@ from django.utils import timezone from django.test import TestCase import responses -from bookwyrm import books_manager, models +from bookwyrm import models +from bookwyrm.connectors import connector_manager from bookwyrm.connectors.abstract_connector import SearchResult @@ -134,7 +135,7 @@ class ImportJob(TestCase): search_url='https://openlibrary.org/search?q=', priority=3, ) - connector = books_manager.load_connector(connector_info) + connector = connector_manager.load_connector(connector_info) result = SearchResult( title='Test Result', key='https://openlibrary.org/works/OL1234W', @@ -163,7 +164,7 @@ class ImportJob(TestCase): json={'name': 'test author'}, status=200) - with patch('bookwyrm.books_manager.first_search_result') as search: + with patch('bookwyrm.connector_manager.first_search_result') as search: search.return_value = result book = self.item_1.get_book_from_isbn() diff --git a/bookwyrm/tests/test_views.py b/bookwyrm/tests/test_views.py index 51c6f502b..8767f7857 100644 --- a/bookwyrm/tests/test_views.py +++ b/bookwyrm/tests/test_views.py @@ -193,7 +193,7 @@ class Views(TestCase): request = self.factory.get('', {'q': 'Test Book'}) with patch('bookwyrm.views.is_api_request') as is_api: is_api.return_value = False - with patch('bookwyrm.books_manager.search') as manager: + with patch('bookwyrm.connectors.connector_manager.search') as manager: manager.return_value = [search_result] response = views.search(request) self.assertIsInstance(response, TemplateResponse) diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 44e71e553..eb09a37a1 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -17,11 +17,12 @@ from django.template.response import TemplateResponse from django.utils import timezone from django.views.decorators.http import require_GET, require_POST -from bookwyrm import books_manager, forms, models, outgoing, goodreads_import +from bookwyrm import forms, models, outgoing, goodreads_import +from bookwyrm.connectors import connector_manager from bookwyrm.broadcast import broadcast from bookwyrm.emailing import password_reset_email from bookwyrm.settings import DOMAIN -from bookwyrm.views import get_user_from_username +from bookwyrm.views import get_user_from_username, get_edition @require_POST @@ -210,7 +211,7 @@ def edit_profile(request): def resolve_book(request): ''' figure out the local path to a book from a remote_id ''' remote_id = request.POST.get('remote_id') - connector = books_manager.get_or_create_connector(remote_id) + connector = connector_manager.get_or_create_connector(remote_id) book = connector.get_or_create_book(remote_id) return redirect('/book/%d' % book.id) @@ -369,7 +370,7 @@ def delete_shelf(request, shelf_id): @require_POST def shelve(request): ''' put a on a user's shelf ''' - book = books_manager.get_edition(request.POST['book']) + book = get_edition(request.POST['book']) desired_shelf = models.Shelf.objects.filter( identifier=request.POST['shelf'], @@ -415,7 +416,7 @@ def unshelve(request): @require_POST def start_reading(request, book_id): ''' begin reading a book ''' - book = books_manager.get_edition(book_id) + book = get_edition(book_id) shelf = models.Shelf.objects.filter( identifier='reading', user=request.user @@ -451,7 +452,7 @@ def start_reading(request, book_id): @require_POST def finish_reading(request, book_id): ''' a user completed a book, yay ''' - book = books_manager.get_edition(book_id) + book = get_edition(book_id) shelf = models.Shelf.objects.filter( identifier='read', user=request.user diff --git a/bookwyrm/views.py b/bookwyrm/views.py index 27492940e..cfe9282da 100644 --- a/bookwyrm/views.py +++ b/bookwyrm/views.py @@ -13,14 +13,22 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_GET from bookwyrm import outgoing -from bookwyrm.activitypub import ActivitypubResponse -from bookwyrm import forms, models, books_manager +from bookwyrm import forms, models from bookwyrm import goodreads_import +from bookwyrm.activitypub import ActivitypubResponse +from bookwyrm.connectors import connector_manager from bookwyrm.settings import PAGE_LENGTH from bookwyrm.tasks import app from bookwyrm.utils import regex +def get_edition(book_id): + ''' look up a book in the db and return an edition ''' + book = models.Book.objects.select_subclasses().get(id=book_id) + if isinstance(book, models.Work): + book = book.default_edition + return book + def get_user_from_username(username): ''' helper function to resolve a localname or a username to a user ''' # raises DoesNotExist if user is now found @@ -211,7 +219,7 @@ def search(request): if is_api_request(request): # only return local book results via json so we don't cause a cascade - book_results = books_manager.local_search(query) + book_results = connector_manager.local_search(query) return JsonResponse([r.json() for r in book_results], safe=False) # use webfinger for mastodon style account@domain.com username @@ -225,7 +233,7 @@ def search(request): similarity__gt=0.5, ).order_by('-similarity')[:10] - book_results = books_manager.search(query) + book_results = connector_manager.search(query) data = { 'title': 'Search Results', 'book_results': book_results, @@ -645,7 +653,7 @@ def book_page(request, book_id): @require_GET def edit_book_page(request, book_id): ''' info about a book ''' - book = books_manager.get_edition(book_id) + book = get_edition(book_id) if not book.description: book.description = book.parent_work.description data = {