diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 000000000..1a641edd2 --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,50 @@ +name: Mypy + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Analysing the code with mypy + env: + SECRET_KEY: beepbeep + DEBUG: false + USE_HTTPS: true + DOMAIN: your.domain.here + BOOKWYRM_DATABASE_BACKEND: postgres + MEDIA_ROOT: images/ + POSTGRES_PASSWORD: hunter2 + POSTGRES_USER: postgres + POSTGRES_DB: github_actions + POSTGRES_HOST: 127.0.0.1 + CELERY_BROKER: "" + REDIS_BROKER_PORT: 6379 + REDIS_BROKER_PASSWORD: beep + USE_DUMMY_CACHE: true + FLOWER_PORT: 8888 + EMAIL_HOST: "smtp.mailgun.org" + EMAIL_PORT: 587 + EMAIL_HOST_USER: "" + EMAIL_HOST_PASSWORD: "" + EMAIL_USE_TLS: true + ENABLE_PREVIEW_IMAGES: false + ENABLE_THUMBNAIL_GENERATION: true + HTTP_X_FORWARDED_PROTO: false + run: | + mypy bookwyrm celerywyrm diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index c78b4f195..615db440a 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -2,6 +2,8 @@ from dataclasses import dataclass, fields, MISSING from json import JSONEncoder import logging +from typing import Optional, Union, TypeVar, overload, Any + import requests from django.apps import apps @@ -10,12 +12,15 @@ from django.utils.http import http_date from bookwyrm import models from bookwyrm.connectors import ConnectorException, get_data +from bookwyrm.models import base_model from bookwyrm.signatures import make_signature from bookwyrm.settings import DOMAIN, INSTANCE_ACTOR_USERNAME from bookwyrm.tasks import app, MISC logger = logging.getLogger(__name__) +TBookWyrmModel = TypeVar("TBookWyrmModel", bound=base_model.BookWyrmModel) + class ActivitySerializerError(ValueError): """routine problems serializing activitypub json""" @@ -65,7 +70,11 @@ class ActivityObject: id: str type: str - def __init__(self, activity_objects=None, **kwargs): + def __init__( + self, + activity_objects: Optional[list[str, base_model.BookWyrmModel]] = None, + **kwargs: dict[str, Any], + ): """this lets you pass in an object with fields that aren't in the dataclass, which it ignores. Any field in the dataclass is required or has a default value""" @@ -101,13 +110,13 @@ class ActivityObject: # pylint: disable=too-many-locals,too-many-branches,too-many-arguments def to_model( self, - model=None, - instance=None, - allow_create=True, - save=True, - overwrite=True, - allow_external_connections=True, - ): + model: Optional[type[TBookWyrmModel]] = None, + instance: Optional[TBookWyrmModel] = None, + allow_create: bool = True, + save: bool = True, + overwrite: bool = True, + allow_external_connections: bool = True, + ) -> Optional[TBookWyrmModel]: """convert from an activity to a model instance. Args: model: the django model that this object is being converted to (will guess if not known) @@ -296,14 +305,40 @@ def get_model_from_type(activity_type): # pylint: disable=too-many-arguments +@overload def resolve_remote_id( - remote_id, - model=None, - refresh=False, - save=True, - get_activity=False, - allow_external_connections=True, -): + remote_id: str, + model: type[TBookWyrmModel], + refresh: bool = False, + save: bool = True, + get_activity: bool = False, + allow_external_connections: bool = True, +) -> TBookWyrmModel: + ... + + +# pylint: disable=too-many-arguments +@overload +def resolve_remote_id( + remote_id: str, + model: Optional[str] = None, + refresh: bool = False, + save: bool = True, + get_activity: bool = False, + allow_external_connections: bool = True, +) -> base_model.BookWyrmModel: + ... + + +# pylint: disable=too-many-arguments +def resolve_remote_id( + remote_id: str, + model: Optional[Union[str, type[base_model.BookWyrmModel]]] = None, + refresh: bool = False, + save: bool = True, + get_activity: bool = False, + allow_external_connections: bool = True, +) -> base_model.BookWyrmModel: """take a remote_id and return an instance, creating if necessary. Args: remote_id: the unique url for looking up the object in the db or by http model: a string or object representing the model that corresponds to the object diff --git a/bookwyrm/activitypub/book.py b/bookwyrm/activitypub/book.py index d3aca4471..5db0dc3ac 100644 --- a/bookwyrm/activitypub/book.py +++ b/bookwyrm/activitypub/book.py @@ -1,6 +1,6 @@ """ book and author data """ from dataclasses import dataclass, field -from typing import List +from typing import Optional from .base_activity import ActivityObject from .image import Document @@ -11,19 +11,19 @@ from .image import Document class BookData(ActivityObject): """shared fields for all book data and authors""" - openlibraryKey: str = None - inventaireId: str = None - librarythingKey: str = None - goodreadsKey: str = None - bnfId: str = None - viaf: str = None - wikidata: str = None - asin: str = None - aasin: str = None - isfdb: str = None - lastEditedBy: str = None - links: List[str] = field(default_factory=lambda: []) - fileLinks: List[str] = field(default_factory=lambda: []) + openlibraryKey: Optional[str] = None + inventaireId: Optional[str] = None + librarythingKey: Optional[str] = None + goodreadsKey: Optional[str] = None + bnfId: Optional[str] = None + viaf: Optional[str] = None + wikidata: Optional[str] = None + asin: Optional[str] = None + aasin: Optional[str] = None + isfdb: Optional[str] = None + lastEditedBy: Optional[str] = None + links: list[str] = field(default_factory=list) + fileLinks: list[str] = field(default_factory=list) # pylint: disable=invalid-name @@ -35,17 +35,17 @@ class Book(BookData): sortTitle: str = None subtitle: str = None description: str = "" - languages: List[str] = field(default_factory=lambda: []) + languages: list[str] = field(default_factory=list) series: str = "" seriesNumber: str = "" - subjects: List[str] = field(default_factory=lambda: []) - subjectPlaces: List[str] = field(default_factory=lambda: []) + subjects: list[str] = field(default_factory=list) + subjectPlaces: list[str] = field(default_factory=list) - authors: List[str] = field(default_factory=lambda: []) + authors: list[str] = field(default_factory=list) firstPublishedDate: str = "" publishedDate: str = "" - cover: Document = None + cover: Optional[Document] = None type: str = "Book" @@ -58,10 +58,10 @@ class Edition(Book): isbn10: str = "" isbn13: str = "" oclcNumber: str = "" - pages: int = None + pages: Optional[int] = None physicalFormat: str = "" physicalFormatDetail: str = "" - publishers: List[str] = field(default_factory=lambda: []) + publishers: list[str] = field(default_factory=list) editionRank: int = 0 type: str = "Edition" @@ -73,7 +73,7 @@ class Work(Book): """work instance of a book object""" lccn: str = "" - editions: List[str] = field(default_factory=lambda: []) + editions: list[str] = field(default_factory=list) type: str = "Work" @@ -83,12 +83,12 @@ class Author(BookData): """author of a book""" name: str - isni: str = None - viafId: str = None - gutenbergId: str = None - born: str = None - died: str = None - aliases: List[str] = field(default_factory=lambda: []) + isni: Optional[str] = None + viafId: Optional[str] = None + gutenbergId: Optional[str] = None + born: Optional[str] = None + died: Optional[str] = None + aliases: list[str] = field(default_factory=list) bio: str = "" wikipediaLink: str = "" type: str = "Author" diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py index 7afa69921..71f9e42d7 100644 --- a/bookwyrm/activitystreams.py +++ b/bookwyrm/activitystreams.py @@ -329,10 +329,9 @@ def add_status_on_create(sender, instance, created, *args, **kwargs): remove_status_task.delay(instance.id) return - # To avoid creating a zillion unnecessary tasks caused by re-saving the model, - # check if it's actually ready to send before we go. We're trusting this was - # set correctly by the inbox or view - if not instance.ready: + # We don't want to create multiple add_status_tasks for each status, and because + # the transactions are atomic, on_commit won't run until the status is ready to add. + if not created: return # when creating new things, gotta wait on the transaction @@ -343,6 +342,10 @@ def add_status_on_create(sender, instance, created, *args, **kwargs): def add_status_on_create_command(sender, instance, created): """runs this code only after the database commit completes""" + # boosts trigger 'saves" twice, so don't bother duplicating the task + if sender == models.Boost and not created: + return + priority = STREAMS # check if this is an old status, de-prioritize if so # (this will happen if federation is very slow, or, more expectedly, on csv import) diff --git a/bookwyrm/book_search.py b/bookwyrm/book_search.py index 822c87f01..ceb228f40 100644 --- a/bookwyrm/book_search.py +++ b/bookwyrm/book_search.py @@ -1,22 +1,53 @@ """ using a bookwyrm instance as a source of book data """ +from __future__ import annotations from dataclasses import asdict, dataclass from functools import reduce import operator +from typing import Optional, Union, Any, Literal, overload from django.contrib.postgres.search import SearchRank, SearchQuery from django.db.models import F, Q +from django.db.models.query import QuerySet from bookwyrm import models from bookwyrm import connectors from bookwyrm.settings import MEDIA_FULL_URL +@overload +def search( + query: str, + *, + min_confidence: float = 0, + filters: Optional[list[Any]] = None, + return_first: Literal[False], +) -> QuerySet[models.Edition]: + ... + + +@overload +def search( + query: str, + *, + min_confidence: float = 0, + filters: Optional[list[Any]] = None, + return_first: Literal[True], +) -> Optional[models.Edition]: + ... + + # pylint: disable=arguments-differ -def search(query, min_confidence=0, filters=None, return_first=False): +def search( + query: str, + *, + min_confidence: float = 0, + filters: Optional[list[Any]] = None, + return_first: bool = False, +) -> Union[Optional[models.Edition], QuerySet[models.Edition]]: """search your local database""" filters = filters or [] if not query: - return [] + return None if return_first else [] query = query.strip() results = None @@ -66,7 +97,9 @@ def format_search_result(search_result): ).json() -def search_identifiers(query, *filters, return_first=False): +def search_identifiers( + query, *filters, return_first=False +) -> Union[Optional[models.Edition], QuerySet[models.Edition]]: """tries remote_id, isbn; defined as dedupe fields on the model""" if connectors.maybe_isbn(query): # Oh did you think the 'S' in ISBN stood for 'standard'? @@ -87,7 +120,9 @@ def search_identifiers(query, *filters, return_first=False): return results -def search_title_author(query, min_confidence, *filters, return_first=False): +def search_title_author( + query, min_confidence, *filters, return_first=False +) -> QuerySet[models.Edition]: """searches for title and author""" query = SearchQuery(query, config="simple") | SearchQuery(query, config="english") results = ( @@ -122,11 +157,11 @@ class SearchResult: title: str key: str connector: object - view_link: str = None - author: str = None - year: str = None - cover: str = None - confidence: int = 1 + view_link: Optional[str] = None + author: Optional[str] = None + year: Optional[str] = None + cover: Optional[str] = None + confidence: float = 1.0 def __repr__(self): # pylint: disable=consider-using-f-string diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 950bb11f9..8b6dcb885 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -1,5 +1,7 @@ """ functionality outline for a book data connector """ +from __future__ import annotations from abc import ABC, abstractmethod +from typing import Optional, TypedDict, Any, Callable, Union, Iterator from urllib.parse import quote_plus import imghdr import logging @@ -16,33 +18,38 @@ from bookwyrm import activitypub, models, settings from bookwyrm.settings import USER_AGENT from .connector_manager import load_more_data, ConnectorException, raise_not_valid_url from .format_mappings import format_mappings - +from ..book_search import SearchResult logger = logging.getLogger(__name__) +JsonDict = dict[str, Any] + + +class ConnectorResults(TypedDict): + """TypedDict for results returned by connector""" + + connector: AbstractMinimalConnector + results: list[SearchResult] + class AbstractMinimalConnector(ABC): """just the bare bones, for other bookwyrm instances""" - def __init__(self, identifier): + def __init__(self, identifier: str): # load connector settings info = models.Connector.objects.get(identifier=identifier) self.connector = info # the things in the connector model to copy over - self_fields = [ - "base_url", - "books_url", - "covers_url", - "search_url", - "isbn_search_url", - "name", - "identifier", - ] - for field in self_fields: - setattr(self, field, getattr(info, field)) + self.base_url = info.base_url + self.books_url = info.books_url + self.covers_url = info.covers_url + self.search_url = info.search_url + self.isbn_search_url = info.isbn_search_url + self.name = info.name + self.identifier = info.identifier - def get_search_url(self, query): + def get_search_url(self, query: str) -> str: """format the query url""" # Check if the query resembles an ISBN if maybe_isbn(query) and self.isbn_search_url and self.isbn_search_url != "": @@ -54,13 +61,21 @@ class AbstractMinimalConnector(ABC): # searched as free text. This, instead, only searches isbn if it's isbn-y return f"{self.search_url}{quote_plus(query)}" - def process_search_response(self, query, data, min_confidence): + def process_search_response( + self, query: str, data: Any, min_confidence: float + ) -> list[SearchResult]: """Format the search results based on the format of the query""" if maybe_isbn(query): return list(self.parse_isbn_search_data(data))[:10] return list(self.parse_search_data(data, min_confidence))[:10] - async def get_results(self, session, url, min_confidence, query): + async def get_results( + self, + session: aiohttp.ClientSession, + url: str, + min_confidence: float, + query: str, + ) -> Optional[ConnectorResults]: """try this specific connector""" # pylint: disable=line-too-long headers = { @@ -74,55 +89,63 @@ class AbstractMinimalConnector(ABC): async with session.get(url, headers=headers, params=params) as response: if not response.ok: logger.info("Unable to connect to %s: %s", url, response.reason) - return + return None try: raw_data = await response.json() except aiohttp.client_exceptions.ContentTypeError as err: logger.exception(err) - return + return None - return { - "connector": self, - "results": self.process_search_response( + return ConnectorResults( + connector=self, + results=self.process_search_response( query, raw_data, min_confidence ), - } + ) except asyncio.TimeoutError: logger.info("Connection timed out for url: %s", url) except aiohttp.ClientError as err: logger.info(err) + return None @abstractmethod - def get_or_create_book(self, remote_id): + def get_or_create_book(self, remote_id: str) -> Optional[models.Book]: """pull up a book record by whatever means possible""" @abstractmethod - def parse_search_data(self, data, min_confidence): + def parse_search_data( + self, data: Any, min_confidence: float + ) -> Iterator[SearchResult]: """turn the result json from a search into a list""" @abstractmethod - def parse_isbn_search_data(self, data): + def parse_isbn_search_data(self, data: Any) -> Iterator[SearchResult]: """turn the result json from a search into a list""" class AbstractConnector(AbstractMinimalConnector): """generic book data connector""" - def __init__(self, identifier): + generated_remote_link_field = "" + + def __init__(self, identifier: str): super().__init__(identifier) # fields we want to look for in book data to copy over # title we handle separately. - self.book_mappings = [] + self.book_mappings: list[Mapping] = [] + self.author_mappings: list[Mapping] = [] - def get_or_create_book(self, remote_id): + def get_or_create_book(self, remote_id: str) -> Optional[models.Book]: """translate arbitrary json into an Activitypub dataclass""" # first, check if we have the origin_id saved existing = models.Edition.find_existing_by_remote_id( remote_id ) or models.Work.find_existing_by_remote_id(remote_id) if existing: - if hasattr(existing, "default_edition"): + if hasattr(existing, "default_edition") and isinstance( + existing.default_edition, models.Edition + ): return existing.default_edition return existing @@ -154,6 +177,9 @@ class AbstractConnector(AbstractMinimalConnector): ) # this will dedupe automatically work = work_activity.to_model(model=models.Work, overwrite=False) + if not work: + return None + for author in self.get_authors_from_data(work_data): work.authors.add(author) @@ -161,12 +187,21 @@ class AbstractConnector(AbstractMinimalConnector): load_more_data.delay(self.connector.id, work.id) return edition - def get_book_data(self, remote_id): # pylint: disable=no-self-use + def get_book_data(self, remote_id: str) -> JsonDict: # pylint: disable=no-self-use """this allows connectors to override the default behavior""" return get_data(remote_id) - def create_edition_from_data(self, work, edition_data, instance=None): + def create_edition_from_data( + self, + work: models.Work, + edition_data: Union[str, JsonDict], + instance: Optional[models.Edition] = None, + ) -> Optional[models.Edition]: """if we already have the work, we're ready""" + if isinstance(edition_data, str): + # We don't expect a string here + return None + mapped_data = dict_from_mappings(edition_data, self.book_mappings) mapped_data["work"] = work.remote_id edition_activity = activitypub.Edition(**mapped_data) @@ -174,6 +209,9 @@ class AbstractConnector(AbstractMinimalConnector): model=models.Edition, overwrite=False, instance=instance ) + if not edition: + return None + # if we're updating an existing instance, we don't need to load authors if instance: return edition @@ -190,7 +228,9 @@ class AbstractConnector(AbstractMinimalConnector): return edition - def get_or_create_author(self, remote_id, instance=None): + def get_or_create_author( + self, remote_id: str, instance: Optional[models.Author] = None + ) -> Optional[models.Author]: """load that author""" if not instance: existing = models.Author.find_existing_by_remote_id(remote_id) @@ -210,46 +250,51 @@ class AbstractConnector(AbstractMinimalConnector): model=models.Author, overwrite=False, instance=instance ) - def get_remote_id_from_model(self, obj): + def get_remote_id_from_model(self, obj: models.BookDataModel) -> Optional[str]: """given the data stored, how can we look this up""" - return getattr(obj, getattr(self, "generated_remote_link_field")) + remote_id: Optional[str] = getattr(obj, self.generated_remote_link_field) + return remote_id - def update_author_from_remote(self, obj): + def update_author_from_remote(self, obj: models.Author) -> Optional[models.Author]: """load the remote data from this connector and add it to an existing author""" remote_id = self.get_remote_id_from_model(obj) + if not remote_id: + return None return self.get_or_create_author(remote_id, instance=obj) - def update_book_from_remote(self, obj): + def update_book_from_remote(self, obj: models.Edition) -> Optional[models.Edition]: """load the remote data from this connector and add it to an existing book""" remote_id = self.get_remote_id_from_model(obj) + if not remote_id: + return None data = self.get_book_data(remote_id) return self.create_edition_from_data(obj.parent_work, data, instance=obj) @abstractmethod - def is_work_data(self, data): + def is_work_data(self, data: JsonDict) -> bool: """differentiate works and editions""" @abstractmethod - def get_edition_from_work_data(self, data): + def get_edition_from_work_data(self, data: JsonDict) -> JsonDict: """every work needs at least one edition""" @abstractmethod - def get_work_from_edition_data(self, data): + def get_work_from_edition_data(self, data: JsonDict) -> JsonDict: """every edition needs a work""" @abstractmethod - def get_authors_from_data(self, data): + def get_authors_from_data(self, data: JsonDict) -> Iterator[models.Author]: """load author data""" @abstractmethod - def expand_book_data(self, book): + def expand_book_data(self, book: models.Book) -> None: """get more info on a book""" -def dict_from_mappings(data, mappings): +def dict_from_mappings(data: JsonDict, mappings: list[Mapping]) -> JsonDict: """create a dict in Activitypub format, using mappings supplies by the subclass""" - result = {} + result: JsonDict = {} for mapping in mappings: # sometimes there are multiple mappings for one field, don't # overwrite earlier writes in that case @@ -259,7 +304,11 @@ def dict_from_mappings(data, mappings): return result -def get_data(url, params=None, timeout=settings.QUERY_TIMEOUT): +def get_data( + url: str, + params: Optional[dict[str, str]] = None, + timeout: int = settings.QUERY_TIMEOUT, +) -> JsonDict: """wrapper for request.get""" # check if the url is blocked raise_not_valid_url(url) @@ -292,10 +341,15 @@ def get_data(url, params=None, timeout=settings.QUERY_TIMEOUT): logger.info(err) raise ConnectorException(err) + if not isinstance(data, dict): + raise ConnectorException("Unexpected data format") + return data -def get_image(url, timeout=10): +def get_image( + url: str, timeout: int = 10 +) -> Union[tuple[ContentFile[bytes], str], tuple[None, None]]: """wrapper for requesting an image""" raise_not_valid_url(url) try: @@ -325,14 +379,19 @@ def get_image(url, timeout=10): class Mapping: """associate a local database field with a field in an external dataset""" - def __init__(self, local_field, remote_field=None, formatter=None): + def __init__( + self, + local_field: str, + remote_field: Optional[str] = None, + formatter: Optional[Callable[[Any], Any]] = None, + ): noop = lambda x: x self.local_field = local_field self.remote_field = remote_field or local_field self.formatter = formatter or noop - def get_value(self, data): + def get_value(self, data: JsonDict) -> Optional[Any]: """pull a field from incoming json and return the formatted version""" value = data.get(self.remote_field) if not value: @@ -343,7 +402,7 @@ class Mapping: return None -def infer_physical_format(format_text): +def infer_physical_format(format_text: str) -> Optional[str]: """try to figure out what the standardized format is from the free value""" format_text = format_text.lower() if format_text in format_mappings: @@ -356,7 +415,7 @@ def infer_physical_format(format_text): return matches[0] -def unique_physical_format(format_text): +def unique_physical_format(format_text: str) -> Optional[str]: """only store the format if it isn't directly in the format mappings""" format_text = format_text.lower() if format_text in format_mappings: @@ -365,7 +424,7 @@ def unique_physical_format(format_text): return format_text -def maybe_isbn(query): +def maybe_isbn(query: str) -> bool: """check if a query looks like an isbn""" isbn = re.sub(r"[\W_]", "", query) # removes filler characters # ISBNs must be numeric except an ISBN10 checkdigit can be 'X' diff --git a/bookwyrm/connectors/bookwyrm_connector.py b/bookwyrm/connectors/bookwyrm_connector.py index e07a0b281..4064f4b4c 100644 --- a/bookwyrm/connectors/bookwyrm_connector.py +++ b/bookwyrm/connectors/bookwyrm_connector.py @@ -1,4 +1,7 @@ """ using another bookwyrm instance as a source of book data """ +from __future__ import annotations +from typing import Any, Iterator + from bookwyrm import activitypub, models from bookwyrm.book_search import SearchResult from .abstract_connector import AbstractMinimalConnector @@ -7,15 +10,19 @@ from .abstract_connector import AbstractMinimalConnector class Connector(AbstractMinimalConnector): """this is basically just for search""" - def get_or_create_book(self, remote_id): + def get_or_create_book(self, remote_id: str) -> models.Edition: return activitypub.resolve_remote_id(remote_id, model=models.Edition) - def parse_search_data(self, data, min_confidence): + def parse_search_data( + self, data: list[dict[str, Any]], min_confidence: float + ) -> Iterator[SearchResult]: for search_result in data: search_result["connector"] = self yield SearchResult(**search_result) - def parse_isbn_search_data(self, data): + def parse_isbn_search_data( + self, data: list[dict[str, Any]] + ) -> Iterator[SearchResult]: for search_result in data: search_result["connector"] = self yield SearchResult(**search_result) diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index e32da7c00..444a626ba 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -1,8 +1,11 @@ """ interface with whatever connectors the app has """ +from __future__ import annotations import asyncio import importlib import ipaddress import logging +from asyncio import Future +from typing import Iterator, Any, Optional, Union, overload, Literal from urllib.parse import urlparse import aiohttp @@ -12,6 +15,8 @@ from django.db.models import signals from requests import HTTPError from bookwyrm import book_search, models +from bookwyrm.book_search import SearchResult +from bookwyrm.connectors import abstract_connector from bookwyrm.settings import SEARCH_TIMEOUT from bookwyrm.tasks import app, CONNECTORS @@ -22,11 +27,15 @@ class ConnectorException(HTTPError): """when the connector can't do what was asked""" -async def async_connector_search(query, items, min_confidence): +async def async_connector_search( + query: str, + items: list[tuple[str, abstract_connector.AbstractConnector]], + min_confidence: float, +) -> list[Optional[abstract_connector.ConnectorResults]]: """Try a number of requests simultaneously""" timeout = aiohttp.ClientTimeout(total=SEARCH_TIMEOUT) async with aiohttp.ClientSession(timeout=timeout) as session: - tasks = [] + tasks: list[Future[Optional[abstract_connector.ConnectorResults]]] = [] for url, connector in items: tasks.append( asyncio.ensure_future( @@ -35,14 +44,29 @@ async def async_connector_search(query, items, min_confidence): ) results = await asyncio.gather(*tasks) - return results + return list(results) -def search(query, min_confidence=0.1, return_first=False): +@overload +def search( + query: str, *, min_confidence: float = 0.1, return_first: Literal[False] +) -> list[abstract_connector.ConnectorResults]: + ... + + +@overload +def search( + query: str, *, min_confidence: float = 0.1, return_first: Literal[True] +) -> Optional[SearchResult]: + ... + + +def search( + query: str, *, min_confidence: float = 0.1, return_first: bool = False +) -> Union[list[abstract_connector.ConnectorResults], Optional[SearchResult]]: """find books based on arbitrary keywords""" if not query: - return [] - results = [] + return None if return_first else [] items = [] for connector in get_connectors(): @@ -57,8 +81,12 @@ def search(query, min_confidence=0.1, return_first=False): items.append((url, connector)) # load as many results as we can - results = asyncio.run(async_connector_search(query, items, min_confidence)) - results = [r for r in results if r] + # failed requests will return None, so filter those out + results = [ + r + for r in asyncio.run(async_connector_search(query, items, min_confidence)) + if r + ] if return_first: # find the best result from all the responses and return that @@ -66,11 +94,12 @@ def search(query, min_confidence=0.1, return_first=False): all_results = sorted(all_results, key=lambda r: r.confidence, reverse=True) return all_results[0] if all_results else None - # failed requests will return None, so filter those out return results -def first_search_result(query, min_confidence=0.1): +def first_search_result( + query: str, min_confidence: float = 0.1 +) -> Union[models.Edition, SearchResult, None]: """search until you find a result that fits""" # try local search first result = book_search.search(query, min_confidence=min_confidence, return_first=True) @@ -80,13 +109,13 @@ def first_search_result(query, min_confidence=0.1): return search(query, min_confidence=min_confidence, return_first=True) or None -def get_connectors(): +def get_connectors() -> Iterator[abstract_connector.AbstractConnector]: """load all connectors""" for info in models.Connector.objects.filter(active=True).order_by("priority").all(): yield load_connector(info) -def get_or_create_connector(remote_id): +def get_or_create_connector(remote_id: str) -> abstract_connector.AbstractConnector: """get the connector related to the object's server""" url = urlparse(remote_id) identifier = url.netloc @@ -110,7 +139,7 @@ def get_or_create_connector(remote_id): @app.task(queue=CONNECTORS) -def load_more_data(connector_id, book_id): +def load_more_data(connector_id: str, book_id: str) -> None: """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) @@ -119,7 +148,9 @@ def load_more_data(connector_id, book_id): @app.task(queue=CONNECTORS) -def create_edition_task(connector_id, work_id, data): +def create_edition_task( + connector_id: int, work_id: int, data: Union[str, abstract_connector.JsonDict] +) -> None: """separate task for each of the 10,000 editions of LoTR""" connector_info = models.Connector.objects.get(id=connector_id) connector = load_connector(connector_info) @@ -127,23 +158,31 @@ def create_edition_task(connector_id, work_id, data): connector.create_edition_from_data(work, data) -def load_connector(connector_info): +def load_connector( + connector_info: models.Connector, +) -> abstract_connector.AbstractConnector: """instantiate the connector class""" connector = importlib.import_module( f"bookwyrm.connectors.{connector_info.connector_file}" ) - return connector.Connector(connector_info.identifier) + return connector.Connector(connector_info.identifier) # type: ignore[no-any-return] @receiver(signals.post_save, sender="bookwyrm.FederatedServer") # pylint: disable=unused-argument -def create_connector(sender, instance, created, *args, **kwargs): +def create_connector( + sender: Any, + instance: models.FederatedServer, + created: Any, + *args: Any, + **kwargs: Any, +) -> None: """create a connector to an external bookwyrm server""" if instance.application_type == "bookwyrm": get_or_create_connector(f"https://{instance.server_name}") -def raise_not_valid_url(url): +def raise_not_valid_url(url: str) -> None: """do some basic reality checks on the url""" parsed = urlparse(url) if not parsed.scheme in ["http", "https"]: diff --git a/bookwyrm/connectors/inventaire.py b/bookwyrm/connectors/inventaire.py index f3e24c0ec..c08bcdee1 100644 --- a/bookwyrm/connectors/inventaire.py +++ b/bookwyrm/connectors/inventaire.py @@ -1,9 +1,10 @@ """ inventaire data connector """ import re +from typing import Any, Union, Optional, Iterator, Iterable from bookwyrm import models from bookwyrm.book_search import SearchResult -from .abstract_connector import AbstractConnector, Mapping +from .abstract_connector import AbstractConnector, Mapping, JsonDict from .abstract_connector import get_data from .connector_manager import ConnectorException, create_edition_task @@ -13,7 +14,7 @@ class Connector(AbstractConnector): generated_remote_link_field = "inventaire_id" - def __init__(self, identifier): + def __init__(self, identifier: str): super().__init__(identifier) get_first = lambda a: a[0] @@ -60,13 +61,13 @@ class Connector(AbstractConnector): Mapping("died", remote_field="wdt:P570", formatter=get_first), ] + shared_mappings - def get_remote_id(self, value): + def get_remote_id(self, value: str) -> str: """convert an id/uri into a url""" return f"{self.books_url}?action=by-uris&uris={value}" - def get_book_data(self, remote_id): + def get_book_data(self, remote_id: str) -> JsonDict: data = get_data(remote_id) - extracted = list(data.get("entities").values()) + extracted = list(data.get("entities", {}).values()) try: data = extracted[0] except (KeyError, IndexError): @@ -74,10 +75,16 @@ class Connector(AbstractConnector): # flatten the data so that images, uri, and claims are on the same level return { **data.get("claims", {}), - **{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks", "type"]}, + **{ + k: data.get(k) + for k in ["uri", "image", "labels", "sitelinks", "type"] + if k in data + }, } - def parse_search_data(self, data, min_confidence): + def parse_search_data( + self, data: JsonDict, min_confidence: float + ) -> Iterator[SearchResult]: for search_result in data.get("results", []): images = search_result.get("image") cover = f"{self.covers_url}/img/entities/{images[0]}" if images else None @@ -96,7 +103,7 @@ class Connector(AbstractConnector): connector=self, ) - def parse_isbn_search_data(self, data): + def parse_isbn_search_data(self, data: JsonDict) -> Iterator[SearchResult]: """got some data""" results = data.get("entities") if not results: @@ -114,35 +121,44 @@ class Connector(AbstractConnector): connector=self, ) - def is_work_data(self, data): + def is_work_data(self, data: JsonDict) -> bool: return data.get("type") == "work" - def load_edition_data(self, work_uri): + def load_edition_data(self, work_uri: str) -> JsonDict: """get a list of editions for a work""" # pylint: disable=line-too-long url = f"{self.books_url}?action=reverse-claims&property=wdt:P629&value={work_uri}&sort=true" return get_data(url) - def get_edition_from_work_data(self, data): - data = self.load_edition_data(data.get("uri")) + def get_edition_from_work_data(self, data: JsonDict) -> JsonDict: + work_uri = data.get("uri") + if not work_uri: + raise ConnectorException("Invalid URI") + data = self.load_edition_data(work_uri) try: uri = data.get("uris", [])[0] except IndexError: raise ConnectorException("Invalid book data") return self.get_book_data(self.get_remote_id(uri)) - def get_work_from_edition_data(self, data): - uri = data.get("wdt:P629", [None])[0] + def get_work_from_edition_data(self, data: JsonDict) -> JsonDict: + try: + uri = data.get("wdt:P629", [])[0] + except IndexError: + raise ConnectorException("Invalid book data") + if not uri: raise ConnectorException("Invalid book data") return self.get_book_data(self.get_remote_id(uri)) - def get_authors_from_data(self, data): + def get_authors_from_data(self, data: JsonDict) -> Iterator[models.Author]: authors = data.get("wdt:P50", []) for author in authors: - yield self.get_or_create_author(self.get_remote_id(author)) + model = self.get_or_create_author(self.get_remote_id(author)) + if model: + yield model - def expand_book_data(self, book): + def expand_book_data(self, book: models.Book) -> None: work = book # go from the edition to the work, if necessary if isinstance(book, models.Edition): @@ -154,11 +170,16 @@ class Connector(AbstractConnector): # who knows, man return - for edition_uri in edition_options.get("uris"): + for edition_uri in edition_options.get("uris", []): remote_id = self.get_remote_id(edition_uri) create_edition_task.delay(self.connector.id, work.id, remote_id) - def create_edition_from_data(self, work, edition_data, instance=None): + def create_edition_from_data( + self, + work: models.Work, + edition_data: Union[str, JsonDict], + instance: Optional[models.Edition] = None, + ) -> Optional[models.Edition]: """pass in the url as data and then call the version in abstract connector""" if isinstance(edition_data, str): try: @@ -168,22 +189,26 @@ class Connector(AbstractConnector): return None return super().create_edition_from_data(work, edition_data, instance=instance) - def get_cover_url(self, cover_blob, *_): + def get_cover_url( + self, cover_blob: Union[list[JsonDict], JsonDict], *_: Any + ) -> Optional[str]: """format the relative cover url into an absolute one: {"url": "/img/entities/e794783f01b9d4f897a1ea9820b96e00d346994f"} """ # covers may or may not be a list - if isinstance(cover_blob, list) and len(cover_blob) > 0: + if isinstance(cover_blob, list): + if len(cover_blob) == 0: + return None cover_blob = cover_blob[0] cover_id = cover_blob.get("url") - if not cover_id: + if not isinstance(cover_id, str): return None # cover may or may not be an absolute url already if re.match(r"^http", cover_id): return cover_id return f"{self.covers_url}{cover_id}" - def resolve_keys(self, keys): + def resolve_keys(self, keys: Iterable[str]) -> list[str]: """cool, it's "wd:Q3156592" now what the heck does that mean""" results = [] for uri in keys: @@ -191,10 +216,10 @@ class Connector(AbstractConnector): data = self.get_book_data(self.get_remote_id(uri)) except ConnectorException: continue - results.append(get_language_code(data.get("labels"))) + results.append(get_language_code(data.get("labels", {}))) return results - def get_description(self, links): + def get_description(self, links: JsonDict) -> str: """grab an extracted excerpt from wikipedia""" link = links.get("enwiki") if not link: @@ -204,15 +229,15 @@ class Connector(AbstractConnector): data = get_data(url) except ConnectorException: return "" - return data.get("extract") + return data.get("extract", "") - def get_remote_id_from_model(self, obj): + def get_remote_id_from_model(self, obj: models.BookDataModel) -> str: """use get_remote_id to figure out the link from a model obj""" remote_id_value = obj.inventaire_id return self.get_remote_id(remote_id_value) -def get_language_code(options, code="en"): +def get_language_code(options: JsonDict, code: str = "en") -> Any: """when there are a bunch of translation but we need a single field""" result = options.get(code) if result: diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index 0fd786660..4dc6d6ac1 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -1,9 +1,13 @@ """ openlibrary data connector """ import re +from typing import Any, Optional, Union, Iterator, Iterable + +from markdown import markdown from bookwyrm import models from bookwyrm.book_search import SearchResult -from .abstract_connector import AbstractConnector, Mapping +from bookwyrm.utils.sanitizer import clean +from .abstract_connector import AbstractConnector, Mapping, JsonDict from .abstract_connector import get_data, infer_physical_format, unique_physical_format from .connector_manager import ConnectorException, create_edition_task from .openlibrary_languages import languages @@ -14,7 +18,7 @@ class Connector(AbstractConnector): generated_remote_link_field = "openlibrary_link" - def __init__(self, identifier): + def __init__(self, identifier: str): super().__init__(identifier) get_first = lambda a, *args: a[0] @@ -94,14 +98,14 @@ class Connector(AbstractConnector): Mapping("inventaire_id", remote_field="links", formatter=get_inventaire_id), ] - def get_book_data(self, remote_id): + def get_book_data(self, remote_id: str) -> JsonDict: data = get_data(remote_id) if data.get("type", {}).get("key") == "/type/redirect": - remote_id = self.base_url + data.get("location") + remote_id = self.base_url + data.get("location", "") return get_data(remote_id) return data - def get_remote_id_from_data(self, data): + def get_remote_id_from_data(self, data: JsonDict) -> str: """format a url from an openlibrary id field""" try: key = data["key"] @@ -109,10 +113,10 @@ class Connector(AbstractConnector): raise ConnectorException("Invalid book data") return f"{self.books_url}{key}" - def is_work_data(self, data): + def is_work_data(self, data: JsonDict) -> bool: return bool(re.match(r"^[\/\w]+OL\d+W$", data["key"])) - def get_edition_from_work_data(self, data): + def get_edition_from_work_data(self, data: JsonDict) -> JsonDict: try: key = data["key"] except KeyError: @@ -124,7 +128,7 @@ class Connector(AbstractConnector): raise ConnectorException("No editions for work") return edition - def get_work_from_edition_data(self, data): + def get_work_from_edition_data(self, data: JsonDict) -> JsonDict: try: key = data["works"][0]["key"] except (IndexError, KeyError): @@ -132,7 +136,7 @@ class Connector(AbstractConnector): url = f"{self.books_url}{key}" return self.get_book_data(url) - def get_authors_from_data(self, data): + def get_authors_from_data(self, data: JsonDict) -> Iterator[models.Author]: """parse author json and load or create authors""" for author_blob in data.get("authors", []): author_blob = author_blob.get("author", author_blob) @@ -144,7 +148,7 @@ class Connector(AbstractConnector): continue yield author - def get_cover_url(self, cover_blob, size="L"): + def get_cover_url(self, cover_blob: list[str], size: str = "L") -> Optional[str]: """ask openlibrary for the cover""" if not cover_blob: return None @@ -152,8 +156,10 @@ class Connector(AbstractConnector): image_name = f"{cover_id}-{size}.jpg" return f"{self.covers_url}/b/id/{image_name}" - def parse_search_data(self, data, min_confidence): - for idx, search_result in enumerate(data.get("docs")): + def parse_search_data( + self, data: JsonDict, min_confidence: float + ) -> Iterator[SearchResult]: + for idx, search_result in enumerate(data.get("docs", [])): # build the remote id from the openlibrary key key = self.books_url + search_result["key"] author = search_result.get("author_name") or ["Unknown"] @@ -174,7 +180,7 @@ class Connector(AbstractConnector): confidence=confidence, ) - def parse_isbn_search_data(self, data): + def parse_isbn_search_data(self, data: JsonDict) -> Iterator[SearchResult]: for search_result in list(data.values()): # build the remote id from the openlibrary key key = self.books_url + search_result["key"] @@ -188,12 +194,12 @@ class Connector(AbstractConnector): year=search_result.get("publish_date"), ) - def load_edition_data(self, olkey): + def load_edition_data(self, olkey: str) -> JsonDict: """query openlibrary for editions of a work""" url = f"{self.books_url}/works/{olkey}/editions" return self.get_book_data(url) - def expand_book_data(self, book): + def expand_book_data(self, book: models.Book) -> None: work = book # go from the edition to the work, if necessary if isinstance(book, models.Edition): @@ -206,14 +212,14 @@ class Connector(AbstractConnector): # who knows, man return - for edition_data in edition_options.get("entries"): + for edition_data in edition_options.get("entries", []): # does this edition have ANY interesting data? if ignore_edition(edition_data): continue create_edition_task.delay(self.connector.id, work.id, edition_data) -def ignore_edition(edition_data): +def ignore_edition(edition_data: JsonDict) -> bool: """don't load a million editions that have no metadata""" # an isbn, we love to see it if edition_data.get("isbn_13") or edition_data.get("isbn_10"): @@ -232,19 +238,30 @@ def ignore_edition(edition_data): return True -def get_description(description_blob): +def get_description(description_blob: Union[JsonDict, str]) -> str: """descriptions can be a string or a dict""" if isinstance(description_blob, dict): - return description_blob.get("value") - return description_blob + description = markdown(description_blob.get("value", "")) + else: + description = markdown(description_blob) + + if ( + description.startswith("

") + and description.endswith("

") + and description.count("

") == 1 + ): + # If there is just one

tag and it is around the text remove it + return description[len("

") : -len("

")].strip() + + return clean(description) -def get_openlibrary_key(key): +def get_openlibrary_key(key: str) -> str: """convert /books/OL27320736M into OL27320736M""" return key.split("/")[-1] -def get_languages(language_blob): +def get_languages(language_blob: Iterable[JsonDict]) -> list[Optional[str]]: """/language/eng -> English""" langs = [] for lang in language_blob: @@ -252,14 +269,14 @@ def get_languages(language_blob): return langs -def get_dict_field(blob, field_name): +def get_dict_field(blob: Optional[JsonDict], field_name: str) -> Optional[Any]: """extract the isni from the remote id data for the author""" if not blob or not isinstance(blob, dict): return None return blob.get(field_name) -def get_wikipedia_link(links): +def get_wikipedia_link(links: list[Any]) -> Optional[str]: """extract wikipedia links""" if not isinstance(links, list): return None @@ -272,7 +289,7 @@ def get_wikipedia_link(links): return None -def get_inventaire_id(links): +def get_inventaire_id(links: list[Any]) -> Optional[str]: """extract and format inventaire ids""" if not isinstance(links, list): return None @@ -282,11 +299,13 @@ def get_inventaire_id(links): continue if link.get("title") == "inventaire.io": iv_link = link.get("url") + if not isinstance(iv_link, str): + return None return iv_link.split("/")[-1] return None -def pick_default_edition(options): +def pick_default_edition(options: list[JsonDict]) -> Optional[JsonDict]: """favor physical copies with covers in english""" if not options: return None diff --git a/bookwyrm/forms/books.py b/bookwyrm/forms/books.py index 3a3979e2c..4885dc063 100644 --- a/bookwyrm/forms/books.py +++ b/bookwyrm/forms/books.py @@ -111,6 +111,7 @@ class EditionFromWorkForm(CustomForm): model = models.Work fields = [ "title", + "sort_title", "subtitle", "authors", "description", diff --git a/bookwyrm/isbn/RangeMessage.xml b/bookwyrm/isbn/RangeMessage.xml new file mode 100644 index 000000000..619cf1ff7 --- /dev/null +++ b/bookwyrm/isbn/RangeMessage.xml @@ -0,0 +1,7904 @@ + + + + + + + + + + + + + + + +]> + + International ISBN Agency + fa1a5bb4-9703-4910-bd34-2ffe0ae46c45 + Sat, 22 Jul 2023 02:00:37 BST + + + 978 + International ISBN Agency + + + 0000000-5999999 + 1 + + + 6000000-6499999 + 3 + + + 6500000-6599999 + 2 + + + 6600000-6999999 + 0 + + + 7000000-7999999 + 1 + + + 8000000-9499999 + 2 + + + 9500000-9899999 + 3 + + + 9900000-9989999 + 4 + + + 9990000-9999999 + 5 + + + + + 979 + International ISBN Agency + + + 0000000-0999999 + 0 + + + 1000000-1299999 + 2 + + + 1300000-7999999 + 0 + + + 8000000-8999999 + 1 + + + 9000000-9999999 + 0 + + + + + + + 978-0 + English language + + + 0000000-1999999 + 2 + + + 2000000-2279999 + 3 + + + 2280000-2289999 + 4 + + + 2290000-3689999 + 3 + + + 3690000-3699999 + 4 + + + 3700000-6389999 + 3 + + + 6390000-6397999 + 4 + + + 6398000-6399999 + 7 + + + 6400000-6449999 + 3 + + + 6450000-6459999 + 7 + + + 6460000-6479999 + 3 + + + 6480000-6489999 + 7 + + + 6490000-6549999 + 3 + + + 6550000-6559999 + 4 + + + 6560000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9499999 + 6 + + + 9500000-9999999 + 7 + + + + + 978-1 + English language + + + 0000000-0099999 + 3 + + + 0100000-0299999 + 2 + + + 0300000-0349999 + 3 + + + 0350000-0399999 + 4 + + + 0400000-0499999 + 3 + + + 0500000-0699999 + 2 + + + 0700000-0999999 + 4 + + + 1000000-3979999 + 3 + + + 3980000-5499999 + 4 + + + 5500000-6499999 + 5 + + + 6500000-6799999 + 4 + + + 6800000-6859999 + 5 + + + 6860000-7139999 + 4 + + + 7140000-7169999 + 3 + + + 7170000-7319999 + 4 + + + 7320000-7399999 + 7 + + + 7400000-7749999 + 5 + + + 7750000-7753999 + 7 + + + 7754000-7763999 + 5 + + + 7764000-7764999 + 7 + + + 7765000-7769999 + 5 + + + 7770000-7782999 + 7 + + + 7783000-7899999 + 5 + + + 7900000-7999999 + 4 + + + 8000000-8004999 + 5 + + + 8005000-8049999 + 5 + + + 8050000-8379999 + 5 + + + 8380000-8384999 + 7 + + + 8385000-8671999 + 5 + + + 8672000-8675999 + 4 + + + 8676000-8697999 + 5 + + + 8698000-9159999 + 6 + + + 9160000-9165059 + 7 + + + 9165060-9168699 + 6 + + + 9168700-9169079 + 7 + + + 9169080-9195999 + 6 + + + 9196000-9196549 + 7 + + + 9196550-9729999 + 6 + + + 9730000-9877999 + 4 + + + 9878000-9911499 + 6 + + + 9911500-9911999 + 7 + + + 9912000-9989899 + 6 + + + 9989900-9999999 + 7 + + + + + 978-2 + French language + + + 0000000-1999999 + 2 + + + 2000000-3499999 + 3 + + + 3500000-3999999 + 5 + + + 4000000-4869999 + 3 + + + 4870000-4949999 + 6 + + + 4950000-4959999 + 3 + + + 4960000-4966999 + 4 + + + 4967000-4969999 + 5 + + + 4970000-5279999 + 3 + + + 5280000-5299999 + 4 + + + 5300000-6999999 + 3 + + + 7000000-8399999 + 4 + + + 8400000-8999999 + 5 + + + 9000000-9197999 + 6 + + + 9198000-9198099 + 5 + + + 9198100-9199429 + 6 + + + 9199430-9199689 + 7 + + + 9199690-9499999 + 6 + + + 9500000-9999999 + 7 + + + + + 978-3 + German language + + + 0000000-0299999 + 2 + + + 0300000-0339999 + 3 + + + 0340000-0369999 + 4 + + + 0370000-0399999 + 5 + + + 0400000-1999999 + 2 + + + 2000000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9499999 + 6 + + + 9500000-9539999 + 7 + + + 9540000-9699999 + 5 + + + 9700000-9849999 + 7 + + + 9850000-9999999 + 5 + + + + + 978-4 + Japan + + + 0000000-1999999 + 2 + + + 2000000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9499999 + 6 + + + 9500000-9999999 + 7 + + + + + 978-5 + former U.S.S.R + + + 0000000-0049999 + 5 + + + 0050000-0099999 + 4 + + + 0100000-1999999 + 2 + + + 2000000-3619999 + 3 + + + 3620000-3623999 + 4 + + + 3624000-3629999 + 5 + + + 3630000-4209999 + 3 + + + 4210000-4299999 + 4 + + + 4300000-4309999 + 3 + + + 4310000-4399999 + 4 + + + 4400000-4409999 + 3 + + + 4410000-4499999 + 4 + + + 4500000-6039999 + 3 + + + 6040000-6049999 + 7 + + + 6050000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9099999 + 6 + + + 9100000-9199999 + 5 + + + 9200000-9299999 + 4 + + + 9300000-9499999 + 5 + + + 9500000-9500999 + 7 + + + 9501000-9799999 + 4 + + + 9800000-9899999 + 5 + + + 9900000-9909999 + 7 + + + 9910000-9999999 + 4 + + + + + 978-600 + Iran + + + 0000000-0999999 + 2 + + + 1000000-4999999 + 3 + + + 5000000-8999999 + 4 + + + 9000000-9867999 + 5 + + + 9868000-9929999 + 4 + + + 9930000-9959999 + 3 + + + 9960000-9999999 + 5 + + + + + 978-601 + Kazakhstan + + + 0000000-1999999 + 2 + + + 2000000-6999999 + 3 + + + 7000000-7999999 + 4 + + + 8000000-8499999 + 5 + + + 8500000-9999999 + 2 + + + + + 978-602 + Indonesia + + + 0000000-0699999 + 2 + + + 0700000-1399999 + 4 + + + 1400000-1499999 + 5 + + + 1500000-1699999 + 4 + + + 1700000-1999999 + 5 + + + 2000000-4999999 + 3 + + + 5000000-5399999 + 5 + + + 5400000-5999999 + 4 + + + 6000000-6199999 + 5 + + + 6200000-6999999 + 4 + + + 7000000-7499999 + 5 + + + 7500000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-603 + Saudi Arabia + + + 0000000-0499999 + 2 + + + 0500000-4999999 + 2 + + + 5000000-7999999 + 3 + + + 8000000-8999999 + 4 + + + 9000000-9999999 + 5 + + + + + 978-604 + Vietnam + + + 0000000-2999999 + 1 + + + 3000000-3999999 + 3 + + + 4000000-4699999 + 2 + + + 4700000-4979999 + 3 + + + 4980000-4999999 + 4 + + + 5000000-8999999 + 2 + + + 9000000-9799999 + 3 + + + 9800000-9999999 + 4 + + + + + 978-605 + Turkey + + + 0000000-0299999 + 2 + + + 0300000-0399999 + 3 + + + 0400000-0599999 + 2 + + + 0600000-0699999 + 5 + + + 0700000-0999999 + 2 + + + 1000000-1999999 + 3 + + + 2000000-2399999 + 4 + + + 2400000-3999999 + 3 + + + 4000000-5999999 + 4 + + + 6000000-7499999 + 5 + + + 7500000-7999999 + 4 + + + 8000000-8999999 + 5 + + + 9000000-9999999 + 4 + + + + + 978-606 + Romania + + + 0000000-0999999 + 3 + + + 1000000-4999999 + 2 + + + 5000000-7999999 + 3 + + + 8000000-9099999 + 4 + + + 9100000-9199999 + 3 + + + 9200000-9599999 + 5 + + + 9600000-9749999 + 4 + + + 9750000-9999999 + 3 + + + + + 978-607 + Mexico + + + 0000000-3999999 + 2 + + + 4000000-5929999 + 3 + + + 5930000-5999999 + 5 + + + 6000000-7499999 + 3 + + + 7500000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-608 + North Macedonia + + + 0000000-0999999 + 1 + + + 1000000-1999999 + 2 + + + 2000000-4499999 + 3 + + + 4500000-6499999 + 4 + + + 6500000-6999999 + 5 + + + 7000000-9999999 + 1 + + + + + 978-609 + Lithuania + + + 0000000-3999999 + 2 + + + 4000000-7999999 + 3 + + + 8000000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-611 + Thailand + + + 0000000-9999999 + 0 + + + + + 978-612 + Peru + + + 0000000-2999999 + 2 + + + 3000000-3999999 + 3 + + + 4000000-4499999 + 4 + + + 4500000-4999999 + 5 + + + 5000000-5149999 + 4 + + + 5150000-9999999 + 0 + + + + + 978-613 + Mauritius + + + 0000000-9999999 + 1 + + + + + 978-614 + Lebanon + + + 0000000-3999999 + 2 + + + 4000000-7999999 + 3 + + + 8000000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-615 + Hungary + + + 0000000-0999999 + 2 + + + 1000000-4999999 + 3 + + + 5000000-7999999 + 4 + + + 8000000-8999999 + 5 + + + 9000000-9999999 + 0 + + + + + 978-616 + Thailand + + + 0000000-1999999 + 2 + + + 2000000-6999999 + 3 + + + 7000000-8999999 + 4 + + + 9000000-9999999 + 5 + + + + + 978-617 + Ukraine + + + 0000000-4999999 + 2 + + + 5000000-6999999 + 3 + + + 7000000-8999999 + 4 + + + 9000000-9999999 + 5 + + + + + 978-618 + Greece + + + 0000000-1999999 + 2 + + + 2000000-4999999 + 3 + + + 5000000-7999999 + 4 + + + 8000000-9999999 + 5 + + + + + 978-619 + Bulgaria + + + 0000000-1499999 + 2 + + + 1500000-6999999 + 3 + + + 7000000-8999999 + 4 + + + 9000000-9999999 + 5 + + + + + 978-620 + Mauritius + + + 0000000-9999999 + 1 + + + + + 978-621 + Philippines + + + 0000000-2999999 + 2 + + + 3000000-3999999 + 0 + + + 4000000-5999999 + 3 + + + 6000000-7999999 + 0 + + + 8000000-8999999 + 4 + + + 9000000-9499999 + 0 + + + 9500000-9999999 + 5 + + + + + 978-622 + Iran + + + 0000000-1099999 + 2 + + + 1100000-1999999 + 0 + + + 2000000-4249999 + 3 + + + 4250000-5199999 + 0 + + + 5200000-8499999 + 4 + + + 8500000-8999999 + 0 + + + 9000000-9999999 + 5 + + + + + 978-623 + Indonesia + + + 0000000-0999999 + 2 + + + 1000000-1299999 + 0 + + + 1300000-4999999 + 3 + + + 5000000-5249999 + 0 + + + 5250000-8799999 + 4 + + + 8800000-9999999 + 5 + + + + + 978-624 + Sri Lanka + + + 0000000-0499999 + 2 + + + 0500000-1999999 + 0 + + + 2000000-2499999 + 3 + + + 2500000-4999999 + 0 + + + 5000000-6449999 + 4 + + + 6450000-9449999 + 0 + + + 9450000-9999999 + 5 + + + + + 978-625 + Turkey + + + 0000000-0099999 + 2 + + + 0100000-3649999 + 0 + + + 3650000-4429999 + 3 + + + 4430000-4449999 + 5 + + + 4450000-4499999 + 3 + + + 4500000-6349999 + 0 + + + 6350000-7793999 + 4 + + + 7794000-7794999 + 5 + + + 7795000-8499999 + 4 + + + 8500000-9899999 + 0 + + + 9900000-9999999 + 5 + + + + + 978-626 + Taiwan + + + 0000000-0499999 + 2 + + + 0500000-2999999 + 0 + + + 3000000-4999999 + 3 + + + 5000000-6999999 + 0 + + + 7000000-7999999 + 4 + + + 8000000-9499999 + 0 + + + 9500000-9999999 + 5 + + + + + 978-627 + Pakistan + + + 0000000-2999999 + 0 + + + 3000000-3199999 + 2 + + + 3200000-4999999 + 0 + + + 5000000-5249999 + 3 + + + 5250000-7499999 + 0 + + + 7500000-7999999 + 4 + + + 8000000-9999999 + 0 + + + + + 978-628 + Colombia + + + 0000000-0999999 + 2 + + + 1000000-4999999 + 0 + + + 5000000-5499999 + 3 + + + 5500000-7499999 + 0 + + + 7500000-8499999 + 4 + + + 8500000-9499999 + 0 + + + 9500000-9999999 + 5 + + + + + 978-629 + Malaysia + + + 0000000-0299999 + 2 + + + 0300000-4699999 + 0 + + + 4700000-4999999 + 3 + + + 5000000-7499999 + 0 + + + 7500000-7999999 + 4 + + + 8000000-9649999 + 0 + + + 9650000-9999999 + 5 + + + + + 978-630 + Romania + + + 0000000-2999999 + 0 + + + 3000000-3499999 + 3 + + + 3500000-6499999 + 0 + + + 6500000-6849999 + 4 + + + 6850000-9999999 + 0 + + + + + 978-631 + Argentina + + + 0000000-0999999 + 2 + + + 1000000-2999999 + 0 + + + 3000000-3999999 + 3 + + + 4000000-6499999 + 0 + + + 6500000-7499999 + 4 + + + 7500000-8999999 + 0 + + + 9000000-9999999 + 5 + + + + + 978-65 + Brazil + + + 0000000-0199999 + 2 + + + 0200000-2499999 + 0 + + + 2500000-2999999 + 3 + + + 3000000-3029999 + 3 + + + 3030000-4999999 + 0 + + + 5000000-5129999 + 4 + + + 5130000-5349999 + 0 + + + 5350000-6149999 + 4 + + + 6150000-7999999 + 0 + + + 8000000-8182499 + 5 + + + 8182500-8449999 + 0 + + + 8450000-8999999 + 5 + + + 9000000-9024499 + 6 + + + 9024500-9799999 + 0 + + + 9800000-9999999 + 6 + + + + + 978-7 + China, People's Republic + + + 0000000-0999999 + 2 + + + 1000000-4999999 + 3 + + + 5000000-7999999 + 4 + + + 8000000-8999999 + 5 + + + 9000000-9999999 + 6 + + + + + 978-80 + former Czechoslovakia + + + 0000000-1999999 + 2 + + + 2000000-5299999 + 3 + + + 5300000-5499999 + 5 + + + 5500000-6899999 + 3 + + + 6900000-6999999 + 5 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9989999 + 6 + + + 9990000-9999999 + 5 + + + + + 978-81 + India + + + 0000000-1899999 + 2 + + + 1900000-1999999 + 5 + + + 2000000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9999999 + 6 + + + + + 978-82 + Norway + + + 0000000-1999999 + 2 + + + 2000000-6899999 + 3 + + + 6900000-6999999 + 6 + + + 7000000-8999999 + 4 + + + 9000000-9899999 + 5 + + + 9900000-9999999 + 6 + + + + + 978-83 + Poland + + + 0000000-1999999 + 2 + + + 2000000-5999999 + 3 + + + 6000000-6999999 + 5 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9999999 + 6 + + + + + 978-84 + Spain + + + 0000000-0999999 + 2 + + + 1000000-1049999 + 5 + + + 1050000-1199999 + 4 + + + 1200000-1299999 + 6 + + + 1300000-1399999 + 4 + + + 1400000-1499999 + 3 + + + 1500000-1999999 + 5 + + + 2000000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9199999 + 4 + + + 9200000-9239999 + 6 + + + 9240000-9299999 + 5 + + + 9300000-9499999 + 6 + + + 9500000-9699999 + 5 + + + 9700000-9999999 + 4 + + + + + 978-85 + Brazil + + + 0000000-1999999 + 2 + + + 2000000-4549999 + 3 + + + 4550000-4552999 + 6 + + + 4553000-4559999 + 5 + + + 4560000-5289999 + 3 + + + 5290000-5319999 + 5 + + + 5320000-5339999 + 4 + + + 5340000-5399999 + 3 + + + 5400000-5402999 + 5 + + + 5403000-5403999 + 5 + + + 5404000-5404999 + 6 + + + 5405000-5408999 + 5 + + + 5409000-5409999 + 6 + + + 5410000-5439999 + 5 + + + 5440000-5479999 + 4 + + + 5480000-5499999 + 5 + + + 5500000-5999999 + 4 + + + 6000000-6999999 + 5 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9249999 + 6 + + + 9250000-9449999 + 5 + + + 9450000-9599999 + 4 + + + 9600000-9799999 + 2 + + + 9800000-9999999 + 5 + + + + + 978-86 + former Yugoslavia + + + 0000000-2999999 + 2 + + + 3000000-5999999 + 3 + + + 6000000-7999999 + 4 + + + 8000000-8999999 + 5 + + + 9000000-9999999 + 6 + + + + + 978-87 + Denmark + + + 0000000-2999999 + 2 + + + 3000000-3999999 + 0 + + + 4000000-6499999 + 3 + + + 6500000-6999999 + 0 + + + 7000000-7999999 + 4 + + + 8000000-8499999 + 0 + + + 8500000-9499999 + 5 + + + 9500000-9699999 + 0 + + + 9700000-9999999 + 6 + + + + + 978-88 + Italy + + + 0000000-1999999 + 2 + + + 2000000-3119999 + 3 + + + 3120000-3149999 + 5 + + + 3150000-3189999 + 3 + + + 3190000-3229999 + 5 + + + 3230000-3269999 + 3 + + + 3270000-3389999 + 4 + + + 3390000-3609999 + 3 + + + 3610000-3629999 + 4 + + + 3630000-5489999 + 3 + + + 5490000-5549999 + 4 + + + 5550000-5999999 + 3 + + + 6000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9099999 + 6 + + + 9100000-9269999 + 3 + + + 9270000-9399999 + 4 + + + 9400000-9479999 + 6 + + + 9480000-9999999 + 5 + + + + + 978-89 + Korea, Republic + + + 0000000-2499999 + 2 + + + 2500000-5499999 + 3 + + + 5500000-8499999 + 4 + + + 8500000-9499999 + 5 + + + 9500000-9699999 + 6 + + + 9700000-9899999 + 5 + + + 9900000-9999999 + 3 + + + + + 978-90 + Netherlands + + + 0000000-1999999 + 2 + + + 2000000-4999999 + 3 + + + 5000000-6999999 + 4 + + + 7000000-7999999 + 5 + + + 8000000-8499999 + 6 + + + 8500000-8999999 + 4 + + + 9000000-9099999 + 2 + + + 9100000-9399999 + 0 + + + 9400000-9499999 + 2 + + + 9500000-9999999 + 0 + + + + + 978-91 + Sweden + + + 0000000-1999999 + 1 + + + 2000000-4999999 + 2 + + + 5000000-6499999 + 3 + + + 6500000-6999999 + 0 + + + 7000000-8199999 + 4 + + + 8200000-8499999 + 0 + + + 8500000-9499999 + 5 + + + 9500000-9699999 + 0 + + + 9700000-9999999 + 6 + + + + + 978-92 + International NGO Publishers and EU Organizations + + + 0000000-5999999 + 1 + + + 6000000-7999999 + 2 + + + 8000000-8999999 + 3 + + + 9000000-9499999 + 4 + + + 9500000-9899999 + 5 + + + 9900000-9999999 + 6 + + + + + 978-93 + India + + + 0000000-0999999 + 2 + + + 1000000-4999999 + 3 + + + 5000000-7999999 + 4 + + + 8000000-9599999 + 5 + + + 9600000-9999999 + 6 + + + + + 978-94 + Netherlands + + + 0000000-5999999 + 3 + + + 6000000-8999999 + 4 + + + 9000000-9999999 + 5 + + + + + 978-950 + Argentina + + + 0000000-4999999 + 2 + + + 5000000-8999999 + 3 + + + 9000000-9899999 + 4 + + + 9900000-9999999 + 5 + + + + + 978-951 + Finland + + + 0000000-1999999 + 1 + + + 2000000-5499999 + 2 + + + 5500000-8899999 + 3 + + + 8900000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-952 + Finland + + + 0000000-1999999 + 2 + + + 2000000-4999999 + 3 + + + 5000000-5999999 + 4 + + + 6000000-6499999 + 2 + + + 6500000-6599999 + 5 + + + 6600000-6699999 + 4 + + + 6700000-6999999 + 5 + + + 7000000-7999999 + 4 + + + 8000000-9499999 + 2 + + + 9500000-9899999 + 4 + + + 9900000-9999999 + 5 + + + + + 978-953 + Croatia + + + 0000000-0999999 + 1 + + + 1000000-1499999 + 2 + + + 1500000-4799999 + 3 + + + 4800000-4999999 + 5 + + + 5000000-5009999 + 3 + + + 5010000-5099999 + 5 + + + 5100000-5499999 + 2 + + + 5500000-5999999 + 5 + + + 6000000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-954 + Bulgaria + + + 0000000-2899999 + 2 + + + 2900000-2999999 + 4 + + + 3000000-7999999 + 3 + + + 8000000-8999999 + 4 + + + 9000000-9299999 + 5 + + + 9300000-9999999 + 4 + + + + + 978-955 + Sri Lanka + + + 0000000-1999999 + 4 + + + 2000000-3399999 + 2 + + + 3400000-3549999 + 4 + + + 3550000-3599999 + 5 + + + 3600000-3799999 + 4 + + + 3800000-3899999 + 5 + + + 3900000-4099999 + 4 + + + 4100000-4499999 + 5 + + + 4500000-4999999 + 4 + + + 5000000-5499999 + 5 + + + 5500000-7109999 + 3 + + + 7110000-7149999 + 5 + + + 7150000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-956 + Chile + + + 0000000-0899999 + 2 + + + 0900000-0999999 + 5 + + + 1000000-1999999 + 2 + + + 2000000-5999999 + 3 + + + 6000000-6999999 + 4 + + + 7000000-9999999 + 4 + + + + + 978-957 + Taiwan + + + 0000000-0299999 + 2 + + + 0300000-0499999 + 4 + + + 0500000-1999999 + 2 + + + 2000000-2099999 + 4 + + + 2100000-2799999 + 2 + + + 2800000-3099999 + 5 + + + 3100000-4399999 + 2 + + + 4400000-8199999 + 3 + + + 8200000-9699999 + 4 + + + 9700000-9999999 + 5 + + + + + 978-958 + Colombia + + + 0000000-4999999 + 2 + + + 5000000-5099999 + 3 + + + 5100000-5199999 + 4 + + + 5200000-5399999 + 5 + + + 5400000-5599999 + 4 + + + 5600000-5999999 + 5 + + + 6000000-7999999 + 3 + + + 8000000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-959 + Cuba + + + 0000000-1999999 + 2 + + + 2000000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-9999999 + 5 + + + + + 978-960 + Greece + + + 0000000-1999999 + 2 + + + 2000000-6599999 + 3 + + + 6600000-6899999 + 4 + + + 6900000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-9299999 + 5 + + + 9300000-9399999 + 2 + + + 9400000-9799999 + 4 + + + 9800000-9999999 + 5 + + + + + 978-961 + Slovenia + + + 0000000-1999999 + 2 + + + 2000000-5999999 + 3 + + + 6000000-8999999 + 4 + + + 9000000-9799999 + 5 + + + 9800000-9999999 + 0 + + + + + 978-962 + Hong Kong, China + + + 0000000-1999999 + 2 + + + 2000000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-8699999 + 5 + + + 8700000-8999999 + 4 + + + 9000000-9999999 + 3 + + + + + 978-963 + Hungary + + + 0000000-1999999 + 2 + + + 2000000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9999999 + 4 + + + + + 978-964 + Iran + + + 0000000-1499999 + 2 + + + 1500000-2499999 + 3 + + + 2500000-2999999 + 4 + + + 3000000-5499999 + 3 + + + 5500000-8999999 + 4 + + + 9000000-9699999 + 5 + + + 9700000-9899999 + 3 + + + 9900000-9999999 + 4 + + + + + 978-965 + Israel + + + 0000000-1999999 + 2 + + + 2000000-5999999 + 3 + + + 6000000-6999999 + 0 + + + 7000000-7999999 + 4 + + + 8000000-8999999 + 0 + + + 9000000-9999999 + 5 + + + + + 978-966 + Ukraine + + + 0000000-1299999 + 2 + + + 1300000-1399999 + 3 + + + 1400000-1499999 + 2 + + + 1500000-1699999 + 4 + + + 1700000-1999999 + 3 + + + 2000000-2789999 + 4 + + + 2790000-2899999 + 3 + + + 2900000-2999999 + 4 + + + 3000000-6999999 + 3 + + + 7000000-8999999 + 4 + + + 9000000-9099999 + 5 + + + 9100000-9499999 + 3 + + + 9500000-9799999 + 5 + + + 9800000-9999999 + 3 + + + + + 978-967 + Malaysia + + + 0000000-0999999 + 4 + + + 1000000-1999999 + 5 + + + 2000000-2499999 + 4 + + + 2500000-2549999 + 3 + + + 2550000-2699999 + 5 + + + 2700000-2799999 + 4 + + + 2800000-2999999 + 4 + + + 3000000-4999999 + 3 + + + 5000000-5999999 + 4 + + + 6000000-8999999 + 2 + + + 9000000-9899999 + 3 + + + 9900000-9989999 + 4 + + + 9990000-9999999 + 5 + + + + + 978-968 + Mexico + + + 0100000-3999999 + 2 + + + 4000000-4999999 + 3 + + + 5000000-7999999 + 4 + + + 8000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-969 + Pakistan + + + 0000000-1999999 + 1 + + + 2000000-2099999 + 2 + + + 2100000-2199999 + 3 + + + 2200000-2299999 + 4 + + + 2300000-2399999 + 5 + + + 2400000-3999999 + 2 + + + 4000000-7499999 + 3 + + + 7500000-9999999 + 4 + + + + + 978-970 + Mexico + + + 0100000-5999999 + 2 + + + 6000000-8999999 + 3 + + + 9000000-9099999 + 4 + + + 9100000-9699999 + 5 + + + 9700000-9999999 + 4 + + + + + 978-971 + Philippines + + + 0000000-0159999 + 3 + + + 0160000-0199999 + 4 + + + 0200000-0299999 + 2 + + + 0300000-0599999 + 4 + + + 0600000-4999999 + 2 + + + 5000000-8499999 + 3 + + + 8500000-9099999 + 4 + + + 9100000-9599999 + 5 + + + 9600000-9699999 + 4 + + + 9700000-9899999 + 2 + + + 9900000-9999999 + 4 + + + + + 978-972 + Portugal + + + 0000000-1999999 + 1 + + + 2000000-5499999 + 2 + + + 5500000-7999999 + 3 + + + 8000000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-973 + Romania + + + 0000000-0999999 + 1 + + + 1000000-1699999 + 3 + + + 1700000-1999999 + 4 + + + 2000000-5499999 + 2 + + + 5500000-7599999 + 3 + + + 7600000-8499999 + 4 + + + 8500000-8899999 + 5 + + + 8900000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-974 + Thailand + + + 0000000-1999999 + 2 + + + 2000000-6999999 + 3 + + + 7000000-8499999 + 4 + + + 8500000-8999999 + 5 + + + 9000000-9499999 + 5 + + + 9500000-9999999 + 4 + + + + + 978-975 + Turkey + + + 0000000-0199999 + 5 + + + 0200000-2399999 + 2 + + + 2400000-2499999 + 4 + + + 2500000-5999999 + 3 + + + 6000000-9199999 + 4 + + + 9200000-9899999 + 5 + + + 9900000-9999999 + 3 + + + + + 978-976 + Caribbean Community + + + 0000000-3999999 + 1 + + + 4000000-5999999 + 2 + + + 6000000-7999999 + 3 + + + 8000000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-977 + Egypt + + + 0000000-1999999 + 2 + + + 2000000-4999999 + 3 + + + 5000000-6999999 + 4 + + + 7000000-8499999 + 3 + + + 8500000-8929999 + 5 + + + 8930000-8949999 + 3 + + + 8950000-8999999 + 4 + + + 9000000-9899999 + 2 + + + 9900000-9999999 + 3 + + + + + 978-978 + Nigeria + + + 0000000-1999999 + 3 + + + 2000000-2999999 + 4 + + + 3000000-7799999 + 5 + + + 7800000-7999999 + 3 + + + 8000000-8999999 + 4 + + + 9000000-9999999 + 3 + + + + + 978-979 + Indonesia + + + 0000000-0999999 + 3 + + + 1000000-1499999 + 4 + + + 1500000-1999999 + 5 + + + 2000000-2999999 + 2 + + + 3000000-3999999 + 4 + + + 4000000-7999999 + 3 + + + 8000000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-980 + Venezuela + + + 0000000-1999999 + 2 + + + 2000000-5999999 + 3 + + + 6000000-9999999 + 4 + + + + + 978-981 + Singapore + + + 0000000-1699999 + 2 + + + 1700000-1799999 + 5 + + + 1800000-1999999 + 2 + + + 2000000-2999999 + 3 + + + 3000000-3099999 + 4 + + + 3100000-3999999 + 3 + + + 4000000-9499999 + 4 + + + 9500000-9899999 + 0 + + + 9900000-9999999 + 2 + + + + + 978-982 + South Pacific + + + 0000000-0999999 + 2 + + + 1000000-6999999 + 3 + + + 7000000-8999999 + 2 + + + 9000000-9799999 + 4 + + + 9800000-9999999 + 5 + + + + + 978-983 + Malaysia + + + 0000000-0199999 + 2 + + + 0200000-1999999 + 3 + + + 2000000-3999999 + 4 + + + 4000000-4499999 + 5 + + + 4500000-4999999 + 2 + + + 5000000-7999999 + 2 + + + 8000000-8999999 + 3 + + + 9000000-9899999 + 4 + + + 9900000-9999999 + 5 + + + + + 978-984 + Bangladesh + + + 0000000-3999999 + 2 + + + 4000000-7999999 + 3 + + + 8000000-8999999 + 4 + + + 9000000-9999999 + 5 + + + + + 978-985 + Belarus + + + 0000000-3999999 + 2 + + + 4000000-5999999 + 3 + + + 6000000-8799999 + 4 + + + 8800000-8999999 + 3 + + + 9000000-9999999 + 5 + + + + + 978-986 + Taiwan + + + 0000000-0599999 + 2 + + + 0600000-0699999 + 5 + + + 0700000-0799999 + 4 + + + 0800000-1199999 + 2 + + + 1200000-5399999 + 3 + + + 5400000-7999999 + 4 + + + 8000000-9999999 + 5 + + + + + 978-987 + Argentina + + + 0000000-0999999 + 2 + + + 1000000-1999999 + 4 + + + 2000000-2999999 + 5 + + + 3000000-3599999 + 2 + + + 3600000-4199999 + 4 + + + 4200000-4399999 + 2 + + + 4400000-4499999 + 4 + + + 4500000-4899999 + 5 + + + 4900000-4999999 + 4 + + + 5000000-8249999 + 3 + + + 8250000-8279999 + 4 + + + 8280000-8299999 + 5 + + + 8300000-8499999 + 4 + + + 8500000-8899999 + 2 + + + 8900000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-988 + Hong Kong, China + + + 0000000-1199999 + 2 + + + 1200000-1999999 + 5 + + + 2000000-6999999 + 3 + + + 7000000-7999999 + 5 + + + 8000000-9699999 + 4 + + + 9700000-9999999 + 5 + + + + + 978-989 + Portugal + + + 0000000-1999999 + 1 + + + 2000000-3499999 + 2 + + + 3500000-3699999 + 5 + + + 3700000-5299999 + 2 + + + 5300000-5499999 + 5 + + + 5500000-7999999 + 3 + + + 8000000-9499999 + 4 + + + 9500000-9999999 + 5 + + + + + 978-9910 + Uzbekistan + + + 0000000-7299999 + 0 + + + 7300000-7499999 + 3 + + + 7500000-9649999 + 0 + + + 9650000-9999999 + 4 + + + + + 978-9911 + Montenegro + + + 0000000-1999999 + 0 + + + 2000000-2499999 + 2 + + + 2500000-5499999 + 0 + + + 5500000-7499999 + 3 + + + 7500000-9999999 + 0 + + + + + 978-9912 + Tanzania + + + 0000000-3999999 + 0 + + + 4000000-4499999 + 2 + + + 4500000-7499999 + 0 + + + 7500000-7999999 + 3 + + + 8000000-9799999 + 0 + + + 9800000-9999999 + 4 + + + + + 978-9913 + Uganda + + + 0000000-0799999 + 2 + + + 0800000-5999999 + 0 + + + 6000000-6999999 + 3 + + + 7000000-9549999 + 0 + + + 9550000-9999999 + 4 + + + + + 978-9914 + Kenya + + + 0000000-3999999 + 0 + + + 4000000-5299999 + 2 + + + 5300000-6999999 + 0 + + + 7000000-7749999 + 3 + + + 7750000-9599999 + 0 + + + 9600000-9999999 + 4 + + + + + 978-9915 + Uruguay + + + 0000000-3999999 + 0 + + + 4000000-5999999 + 2 + + + 6000000-6499999 + 0 + + + 6500000-7999999 + 3 + + + 8000000-9299999 + 0 + + + 9300000-9999999 + 4 + + + + + 978-9916 + Estonia + + + 0000000-0999999 + 1 + + + 1000000-3999999 + 2 + + + 4000000-5999999 + 1 + + + 6000000-7999999 + 3 + + + 8000000-8499999 + 2 + + + 8500000-8999999 + 3 + + + 9000000-9249999 + 0 + + + 9250000-9999999 + 4 + + + + + 978-9917 + Bolivia + + + 0000000-0999999 + 1 + + + 1000000-2999999 + 0 + + + 3000000-3499999 + 2 + + + 3500000-5999999 + 0 + + + 6000000-6999999 + 3 + + + 7000000-9799999 + 0 + + + 9800000-9999999 + 4 + + + + + 978-9918 + Malta + + + 0000000-0999999 + 1 + + + 1000000-1999999 + 0 + + + 2000000-2999999 + 2 + + + 3000000-5999999 + 0 + + + 6000000-7999999 + 3 + + + 8000000-9499999 + 0 + + + 9500000-9999999 + 4 + + + + + 978-9919 + Mongolia + + + 0000000-0999999 + 1 + + + 1000000-1999999 + 0 + + + 2000000-2999999 + 2 + + + 3000000-4999999 + 0 + + + 5000000-5999999 + 3 + + + 6000000-8999999 + 0 + + + 9000000-9999999 + 4 + + + + + 978-9920 + Morocco + + + 0000000-2999999 + 0 + + + 3000000-4299999 + 2 + + + 4300000-4999999 + 0 + + + 5000000-7999999 + 3 + + + 8000000-8749999 + 0 + + + 8750000-9999999 + 4 + + + + + 978-9921 + Kuwait + + + 0000000-0999999 + 1 + + + 1000000-2999999 + 0 + + + 3000000-3999999 + 2 + + + 4000000-6999999 + 0 + + + 7000000-8999999 + 3 + + + 9000000-9699999 + 0 + + + 9700000-9999999 + 4 + + + + + 978-9922 + Iraq + + + 0000000-1999999 + 0 + + + 2000000-2999999 + 2 + + + 3000000-5999999 + 0 + + + 6000000-7999999 + 3 + + + 8000000-8499999 + 0 + + + 8500000-9999999 + 4 + + + + + 978-9923 + Jordan + + + 0000000-0999999 + 1 + + + 1000000-5999999 + 2 + + + 6000000-6999999 + 0 + + + 7000000-8999999 + 3 + + + 9000000-9399999 + 0 + + + 9400000-9999999 + 4 + + + + + 978-9924 + Cambodia + + + 0000000-2999999 + 0 + + + 3000000-3999999 + 2 + + + 4000000-4999999 + 0 + + + 5000000-6499999 + 3 + + + 6500000-8999999 + 0 + + + 9000000-9999999 + 4 + + + + + 978-9925 + Cyprus + + + 0000000-2999999 + 1 + + + 3000000-5499999 + 2 + + + 5500000-7349999 + 3 + + + 7350000-9999999 + 4 + + + + + 978-9926 + Bosnia and Herzegovina + + + 0000000-1999999 + 1 + + + 2000000-3999999 + 2 + + + 4000000-7999999 + 3 + + + 8000000-9999999 + 4 + + + + + 978-9927 + Qatar + + + 0000000-0999999 + 2 + + + 1000000-3999999 + 3 + + + 4000000-4999999 + 4 + + + 5000000-9999999 + 0 + + + + + 978-9928 + Albania + + + 0000000-0999999 + 2 + + + 1000000-3999999 + 3 + + + 4000000-4999999 + 4 + + + 5000000-7999999 + 0 + + + 8000000-8999999 + 3 + + + 9000000-9999999 + 2 + + + + + 978-9929 + Guatemala + + + 0000000-3999999 + 1 + + + 4000000-5499999 + 2 + + + 5500000-7999999 + 3 + + + 8000000-9999999 + 4 + + + + + 978-9930 + Costa Rica + + + 0000000-4999999 + 2 + + + 5000000-9399999 + 3 + + + 9400000-9999999 + 4 + + + + + 978-9931 + Algeria + + + 0000000-2399999 + 2 + + + 2400000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9932 + Lao People's Democratic Republic + + + 0000000-3999999 + 2 + + + 4000000-8499999 + 3 + + + 8500000-9999999 + 4 + + + + + 978-9933 + Syria + + + 0000000-0999999 + 1 + + + 1000000-3999999 + 2 + + + 4000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9934 + Latvia + + + 0000000-0999999 + 1 + + + 1000000-4999999 + 2 + + + 5000000-7999999 + 3 + + + 8000000-9999999 + 4 + + + + + 978-9935 + Iceland + + + 0000000-0999999 + 1 + + + 1000000-3999999 + 2 + + + 4000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9936 + Afghanistan + + + 0000000-1999999 + 1 + + + 2000000-3999999 + 2 + + + 4000000-7999999 + 3 + + + 8000000-9999999 + 4 + + + + + 978-9937 + Nepal + + + 0000000-2999999 + 1 + + + 3000000-4999999 + 2 + + + 5000000-7999999 + 3 + + + 8000000-9999999 + 4 + + + + + 978-9938 + Tunisia + + + 0000000-7999999 + 2 + + + 8000000-9499999 + 3 + + + 9500000-9749999 + 4 + + + 9750000-9909999 + 3 + + + 9910000-9999999 + 4 + + + + + 978-9939 + Armenia + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-8999999 + 3 + + + 9000000-9599999 + 4 + + + 9600000-9799999 + 3 + + + 9800000-9999999 + 2 + + + + + 978-9940 + Montenegro + + + 0000000-1999999 + 1 + + + 2000000-4999999 + 2 + + + 5000000-8399999 + 3 + + + 8400000-8699999 + 2 + + + 8700000-9999999 + 4 + + + + + 978-9941 + Georgia + + + 0000000-0999999 + 1 + + + 1000000-3999999 + 2 + + + 4000000-7999999 + 3 + + + 8000000-8999999 + 1 + + + 9000000-9999999 + 4 + + + + + 978-9942 + Ecuador + + + 0000000-5999999 + 2 + + + 6000000-6999999 + 3 + + + 7000000-7499999 + 4 + + + 7500000-8499999 + 3 + + + 8500000-8999999 + 4 + + + 9000000-9849999 + 3 + + + 9850000-9999999 + 4 + + + + + 978-9943 + Uzbekistan + + + 0000000-2999999 + 2 + + + 3000000-3999999 + 3 + + + 4000000-9749999 + 4 + + + 9750000-9999999 + 3 + + + + + 978-9944 + Turkey + + + 0000000-0999999 + 4 + + + 1000000-4999999 + 3 + + + 5000000-5999999 + 4 + + + 6000000-6999999 + 2 + + + 7000000-7999999 + 3 + + + 8000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-9945 + Dominican Republic + + + 0000000-0099999 + 2 + + + 0100000-0799999 + 3 + + + 0800000-3999999 + 2 + + + 4000000-5699999 + 3 + + + 5700000-5799999 + 2 + + + 5800000-7999999 + 3 + + + 8000000-8099999 + 2 + + + 8100000-8499999 + 3 + + + 8500000-9999999 + 4 + + + + + 978-9946 + Korea, P.D.R. + + + 0000000-1999999 + 1 + + + 2000000-3999999 + 2 + + + 4000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9947 + Algeria + + + 0000000-1999999 + 1 + + + 2000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-9948 + United Arab Emirates + + + 0000000-3999999 + 2 + + + 4000000-8499999 + 3 + + + 8500000-9999999 + 4 + + + + + 978-9949 + Estonia + + + 0000000-0899999 + 2 + + + 0900000-0999999 + 3 + + + 1000000-3999999 + 2 + + + 4000000-6999999 + 3 + + + 7000000-7199999 + 2 + + + 7200000-7499999 + 4 + + + 7500000-8999999 + 2 + + + 9000000-9999999 + 4 + + + + + 978-9950 + Palestine + + + 0000000-2999999 + 2 + + + 3000000-8499999 + 3 + + + 8500000-9999999 + 4 + + + + + 978-9951 + Kosova + + + 0000000-3899999 + 2 + + + 3900000-8499999 + 3 + + + 8500000-9799999 + 4 + + + 9800000-9999999 + 3 + + + + + 978-9952 + Azerbaijan + + + 0000000-1999999 + 1 + + + 2000000-3999999 + 2 + + + 4000000-7999999 + 3 + + + 8000000-9999999 + 4 + + + + + 978-9953 + Lebanon + + + 0000000-0999999 + 1 + + + 1000000-3999999 + 2 + + + 4000000-5999999 + 3 + + + 6000000-8999999 + 2 + + + 9000000-9299999 + 4 + + + 9300000-9699999 + 2 + + + 9700000-9999999 + 3 + + + + + 978-9954 + Morocco + + + 0000000-1999999 + 1 + + + 2000000-3999999 + 2 + + + 4000000-7999999 + 3 + + + 8000000-9899999 + 4 + + + 9900000-9999999 + 2 + + + + + 978-9955 + Lithuania + + + 0000000-3999999 + 2 + + + 4000000-9299999 + 3 + + + 9300000-9999999 + 4 + + + + + 978-9956 + Cameroon + + + 0000000-0999999 + 1 + + + 1000000-3999999 + 2 + + + 4000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9957 + Jordan + + + 0000000-3999999 + 2 + + + 4000000-6499999 + 3 + + + 6500000-6799999 + 2 + + + 6800000-6999999 + 3 + + + 7000000-8499999 + 2 + + + 8500000-8799999 + 4 + + + 8800000-9999999 + 2 + + + + + 978-9958 + Bosnia and Herzegovina + + + 0000000-0199999 + 2 + + + 0200000-0299999 + 3 + + + 0300000-0399999 + 4 + + + 0400000-0899999 + 3 + + + 0900000-0999999 + 4 + + + 1000000-1899999 + 2 + + + 1900000-1999999 + 4 + + + 2000000-4999999 + 2 + + + 5000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9959 + Libya + + + 0000000-1999999 + 1 + + + 2000000-7999999 + 2 + + + 8000000-9499999 + 3 + + + 9500000-9699999 + 4 + + + 9700000-9799999 + 3 + + + 9800000-9999999 + 2 + + + + + 978-9960 + Saudi Arabia + + + 0000000-5999999 + 2 + + + 6000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9961 + Algeria + + + 0000000-2999999 + 1 + + + 3000000-6999999 + 2 + + + 7000000-9499999 + 3 + + + 9500000-9999999 + 4 + + + + + 978-9962 + Panama + + + 0000000-5499999 + 2 + + + 5500000-5599999 + 4 + + + 5600000-5999999 + 2 + + + 6000000-8499999 + 3 + + + 8500000-9999999 + 4 + + + + + 978-9963 + Cyprus + + + 0000000-1999999 + 1 + + + 2000000-2499999 + 4 + + + 2500000-2799999 + 3 + + + 2800000-2999999 + 4 + + + 3000000-5499999 + 2 + + + 5500000-7349999 + 3 + + + 7350000-7499999 + 4 + + + 7500000-9999999 + 4 + + + + + 978-9964 + Ghana + + + 0000000-6999999 + 1 + + + 7000000-9499999 + 2 + + + 9500000-9999999 + 3 + + + + + 978-9965 + Kazakhstan + + + 0000000-3999999 + 2 + + + 4000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9966 + Kenya + + + 0000000-1399999 + 3 + + + 1400000-1499999 + 2 + + + 1500000-1999999 + 4 + + + 2000000-6999999 + 2 + + + 7000000-7499999 + 4 + + + 7500000-8209999 + 3 + + + 8210000-8249999 + 4 + + + 8250000-8259999 + 3 + + + 8260000-8289999 + 4 + + + 8290000-9599999 + 3 + + + 9600000-9999999 + 4 + + + + + 978-9967 + Kyrgyz Republic + + + 0000000-3999999 + 2 + + + 4000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9968 + Costa Rica + + + 0000000-4999999 + 2 + + + 5000000-9399999 + 3 + + + 9400000-9999999 + 4 + + + + + 978-9969 + Algeria + + + 0000000-0699999 + 2 + + + 0700000-4999999 + 0 + + + 5000000-6499999 + 3 + + + 6500000-9699999 + 0 + + + 9700000-9999999 + 4 + + + + + 978-9970 + Uganda + + + 0000000-3999999 + 2 + + + 4000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9971 + Singapore + + + 0000000-5999999 + 1 + + + 6000000-8999999 + 2 + + + 9000000-9899999 + 3 + + + 9900000-9999999 + 4 + + + + + 978-9972 + Peru + + + 0000000-0999999 + 2 + + + 1000000-1999999 + 1 + + + 2000000-2499999 + 3 + + + 2500000-2999999 + 4 + + + 3000000-5999999 + 2 + + + 6000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9973 + Tunisia + + + 0000000-0599999 + 2 + + + 0600000-0899999 + 3 + + + 0900000-0999999 + 4 + + + 1000000-6999999 + 2 + + + 7000000-9699999 + 3 + + + 9700000-9999999 + 4 + + + + + 978-9974 + Uruguay + + + 0000000-2999999 + 1 + + + 3000000-5499999 + 2 + + + 5500000-7499999 + 3 + + + 7500000-8799999 + 4 + + + 8800000-9099999 + 3 + + + 9100000-9499999 + 2 + + + 9500000-9999999 + 2 + + + + + 978-9975 + Moldova + + + 0000000-0999999 + 1 + + + 1000000-2999999 + 3 + + + 3000000-3999999 + 4 + + + 4000000-4499999 + 4 + + + 4500000-8999999 + 2 + + + 9000000-9499999 + 3 + + + 9500000-9999999 + 4 + + + + + 978-9976 + Tanzania + + + 0000000-4999999 + 1 + + + 5000000-5799999 + 4 + + + 5800000-5899999 + 3 + + + 5900000-8999999 + 2 + + + 9000000-9899999 + 3 + + + 9900000-9999999 + 4 + + + + + 978-9977 + Costa Rica + + + 0000000-8999999 + 2 + + + 9000000-9899999 + 3 + + + 9900000-9999999 + 4 + + + + + 978-9978 + Ecuador + + + 0000000-2999999 + 2 + + + 3000000-3999999 + 3 + + + 4000000-9499999 + 2 + + + 9500000-9899999 + 3 + + + 9900000-9999999 + 4 + + + + + 978-9979 + Iceland + + + 0000000-4999999 + 1 + + + 5000000-6499999 + 2 + + + 6500000-6599999 + 3 + + + 6600000-7599999 + 2 + + + 7600000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9980 + Papua New Guinea + + + 0000000-3999999 + 1 + + + 4000000-8999999 + 2 + + + 9000000-9899999 + 3 + + + 9900000-9999999 + 4 + + + + + 978-9981 + Morocco + + + 0000000-0999999 + 2 + + + 1000000-1599999 + 3 + + + 1600000-1999999 + 4 + + + 2000000-7999999 + 2 + + + 8000000-9499999 + 3 + + + 9500000-9999999 + 4 + + + + + 978-9982 + Zambia + + + 0000000-7999999 + 2 + + + 8000000-9899999 + 3 + + + 9900000-9999999 + 4 + + + + + 978-9983 + Gambia + + + 0000000-7999999 + 0 + + + 8000000-9499999 + 2 + + + 9500000-9899999 + 3 + + + 9900000-9999999 + 4 + + + + + 978-9984 + Latvia + + + 0000000-4999999 + 2 + + + 5000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9985 + Estonia + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-8999999 + 3 + + + 9000000-9999999 + 4 + + + + + 978-9986 + Lithuania + + + 0000000-3999999 + 2 + + + 4000000-8999999 + 3 + + + 9000000-9399999 + 4 + + + 9400000-9699999 + 3 + + + 9700000-9999999 + 2 + + + + + 978-9987 + Tanzania + + + 0000000-3999999 + 2 + + + 4000000-8799999 + 3 + + + 8800000-9999999 + 4 + + + + + 978-9988 + Ghana + + + 0000000-3999999 + 1 + + + 4000000-5499999 + 2 + + + 5500000-7499999 + 3 + + + 7500000-9999999 + 4 + + + + + 978-9989 + North Macedonia + + + 0000000-0999999 + 1 + + + 1000000-1999999 + 3 + + + 2000000-2999999 + 4 + + + 3000000-5999999 + 2 + + + 6000000-9499999 + 3 + + + 9500000-9999999 + 4 + + + + + 978-99901 + Bahrain + + + 0000000-4999999 + 2 + + + 5000000-7999999 + 3 + + + 8000000-9999999 + 2 + + + + + 978-99902 + Reserved Agency + + + 0000000-9999999 + 0 + + + + + 978-99903 + Mauritius + + + 0000000-1999999 + 1 + + + 2000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99904 + Curaçao + + + 0000000-5999999 + 1 + + + 6000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99905 + Bolivia + + + 0000000-3999999 + 1 + + + 4000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99906 + Kuwait + + + 0000000-2999999 + 1 + + + 3000000-5999999 + 2 + + + 6000000-6999999 + 3 + + + 7000000-8999999 + 2 + + + 9000000-9499999 + 2 + + + 9500000-9999999 + 3 + + + + + 978-99908 + Malawi + + + 0000000-0999999 + 1 + + + 1000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99909 + Malta + + + 0000000-3999999 + 1 + + + 4000000-9499999 + 2 + + + 9500000-9999999 + 3 + + + + + 978-99910 + Sierra Leone + + + 0000000-2999999 + 1 + + + 3000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99911 + Lesotho + + + 0000000-5999999 + 2 + + + 6000000-9999999 + 3 + + + + + 978-99912 + Botswana + + + 0000000-3999999 + 1 + + + 4000000-5999999 + 3 + + + 6000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99913 + Andorra + + + 0000000-2999999 + 1 + + + 3000000-3599999 + 2 + + + 3600000-5999999 + 0 + + + 6000000-6049999 + 3 + + + 6050000-9999999 + 0 + + + + + 978-99914 + International NGO Publishers + + + 0000000-4999999 + 1 + + + 5000000-6999999 + 2 + + + 7000000-7999999 + 1 + + + 8000000-8699999 + 2 + + + 8700000-8799999 + 3 + + + 8800000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99915 + Maldives + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99916 + Namibia + + + 0000000-2999999 + 1 + + + 3000000-6999999 + 2 + + + 7000000-9999999 + 3 + + + + + 978-99917 + Brunei Darussalam + + + 0000000-2999999 + 1 + + + 3000000-8899999 + 2 + + + 8900000-9999999 + 3 + + + + + 978-99918 + Faroe Islands + + + 0000000-3999999 + 1 + + + 4000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99919 + Benin + + + 0000000-2999999 + 1 + + + 3000000-3999999 + 3 + + + 4000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99920 + Andorra + + + 0000000-4999999 + 1 + + + 5000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99921 + Qatar + + + 0000000-1999999 + 1 + + + 2000000-6999999 + 2 + + + 7000000-7999999 + 3 + + + 8000000-8999999 + 1 + + + 9000000-9999999 + 2 + + + + + 978-99922 + Guatemala + + + 0000000-3999999 + 1 + + + 4000000-6999999 + 2 + + + 7000000-9999999 + 3 + + + + + 978-99923 + El Salvador + + + 0000000-1999999 + 1 + + + 2000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99924 + Nicaragua + + + 0000000-1999999 + 1 + + + 2000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99925 + Paraguay + + + 0000000-0999999 + 1 + + + 1000000-1999999 + 2 + + + 2000000-2999999 + 3 + + + 3000000-3999999 + 1 + + + 4000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99926 + Honduras + + + 0000000-0999999 + 1 + + + 1000000-5999999 + 2 + + + 6000000-8699999 + 3 + + + 8700000-8999999 + 2 + + + 9000000-9999999 + 2 + + + + + 978-99927 + Albania + + + 0000000-2999999 + 1 + + + 3000000-5999999 + 2 + + + 6000000-9999999 + 3 + + + + + 978-99928 + Georgia + + + 0000000-0999999 + 1 + + + 1000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99929 + Mongolia + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99930 + Armenia + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99931 + Seychelles + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99932 + Malta + + + 0000000-0999999 + 1 + + + 1000000-5999999 + 2 + + + 6000000-6999999 + 3 + + + 7000000-7999999 + 1 + + + 8000000-9999999 + 2 + + + + + 978-99933 + Nepal + + + 0000000-2999999 + 1 + + + 3000000-5999999 + 2 + + + 6000000-9999999 + 3 + + + + + 978-99934 + Dominican Republic + + + 0000000-1999999 + 1 + + + 2000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99935 + Haiti + + + 0000000-2999999 + 1 + + + 3000000-5999999 + 2 + + + 6000000-6999999 + 3 + + + 7000000-8999999 + 1 + + + 9000000-9999999 + 2 + + + + + 978-99936 + Bhutan + + + 0000000-0999999 + 1 + + + 1000000-5999999 + 2 + + + 6000000-9999999 + 3 + + + + + 978-99937 + Macau + + + 0000000-1999999 + 1 + + + 2000000-5999999 + 2 + + + 6000000-9999999 + 3 + + + + + 978-99938 + Srpska, Republic of + + + 0000000-1999999 + 1 + + + 2000000-5999999 + 2 + + + 6000000-8999999 + 3 + + + 9000000-9999999 + 2 + + + + + 978-99939 + Guatemala + + + 0000000-2999999 + 1 + + + 3000000-5999999 + 2 + + + 6000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99940 + Georgia + + + 0000000-0999999 + 1 + + + 1000000-6999999 + 2 + + + 7000000-9999999 + 3 + + + + + 978-99941 + Armenia + + + 0000000-2999999 + 1 + + + 3000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99942 + Sudan + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99943 + Albania + + + 0000000-2999999 + 1 + + + 3000000-5999999 + 2 + + + 6000000-9999999 + 3 + + + + + 978-99944 + Ethiopia + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99945 + Namibia + + + 0000000-4999999 + 1 + + + 5000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99946 + Nepal + + + 0000000-2999999 + 1 + + + 3000000-5999999 + 2 + + + 6000000-9999999 + 3 + + + + + 978-99947 + Tajikistan + + + 0000000-2999999 + 1 + + + 3000000-6999999 + 2 + + + 7000000-9999999 + 3 + + + + + 978-99948 + Eritrea + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99949 + Mauritius + + + 0000000-1999999 + 1 + + + 2000000-7999999 + 2 + + + 8000000-8999999 + 1 + + + 9000000-9899999 + 3 + + + 9900000-9999999 + 2 + + + + + 978-99950 + Cambodia + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99951 + Reserved Agency + + + 0000000-9999999 + 0 + + + + + 978-99952 + Mali + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99953 + Paraguay + + + 0000000-2999999 + 1 + + + 3000000-7999999 + 2 + + + 8000000-9399999 + 3 + + + 9400000-9999999 + 2 + + + + + 978-99954 + Bolivia + + + 0000000-2999999 + 1 + + + 3000000-6999999 + 2 + + + 7000000-8799999 + 3 + + + 8800000-9999999 + 2 + + + + + 978-99955 + Srpska, Republic of + + + 0000000-1999999 + 1 + + + 2000000-5999999 + 2 + + + 6000000-7999999 + 3 + + + 8000000-9999999 + 2 + + + + + 978-99956 + Albania + + + 0000000-5999999 + 2 + + + 6000000-8599999 + 3 + + + 8600000-9999999 + 2 + + + + + 978-99957 + Malta + + + 0000000-1999999 + 1 + + + 2000000-7999999 + 2 + + + 8000000-9499999 + 3 + + + 9500000-9999999 + 2 + + + + + 978-99958 + Bahrain + + + 0000000-4999999 + 1 + + + 5000000-9399999 + 2 + + + 9400000-9499999 + 3 + + + 9500000-9999999 + 3 + + + + + 978-99959 + Luxembourg + + + 0000000-2999999 + 1 + + + 3000000-5999999 + 2 + + + 6000000-9999999 + 3 + + + + + 978-99960 + Malawi + + + 0000000-0699999 + 0 + + + 0700000-0999999 + 3 + + + 1000000-9499999 + 2 + + + 9500000-9999999 + 3 + + + + + 978-99961 + El Salvador + + + 0000000-2999999 + 1 + + + 3000000-3699999 + 3 + + + 3700000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99962 + Mongolia + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99963 + Cambodia + + + 0000000-4999999 + 2 + + + 5000000-9199999 + 3 + + + 9200000-9999999 + 2 + + + + + 978-99964 + Nicaragua + + + 0000000-1999999 + 1 + + + 2000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99965 + Macau + + + 0000000-2999999 + 1 + + + 3000000-3599999 + 3 + + + 3600000-6299999 + 2 + + + 6300000-9999999 + 3 + + + + + 978-99966 + Kuwait + + + 0000000-2999999 + 1 + + + 3000000-6999999 + 2 + + + 7000000-7999999 + 3 + + + 8000000-9699999 + 2 + + + 9700000-9999999 + 3 + + + + + 978-99967 + Paraguay + + + 0000000-0999999 + 1 + + + 1000000-5999999 + 2 + + + 6000000-9999999 + 3 + + + + + 978-99968 + Botswana + + + 0000000-3999999 + 1 + + + 4000000-5999999 + 3 + + + 6000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99969 + Oman + + + 0000000-4999999 + 1 + + + 5000000-7999999 + 2 + + + 8000000-9499999 + 3 + + + 9500000-9999999 + 2 + + + + + 978-99970 + Haiti + + + 0000000-4999999 + 1 + + + 5000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99971 + Myanmar + + + 0000000-3999999 + 1 + + + 4000000-8499999 + 2 + + + 8500000-9999999 + 3 + + + + + 978-99972 + Faroe Islands + + + 0000000-4999999 + 1 + + + 5000000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99973 + Mongolia + + + 0000000-3999999 + 1 + + + 4000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99974 + Bolivia + + + 0000000-0999999 + 1 + + + 1000000-2599999 + 2 + + + 2600000-3999999 + 3 + + + 4000000-6399999 + 2 + + + 6400000-6499999 + 3 + + + 6500000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99975 + Tajikistan + + + 0000000-2999999 + 1 + + + 3000000-3999999 + 3 + + + 4000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99976 + Srpska, Republic of + + + 0000000-0999999 + 1 + + + 1000000-1599999 + 2 + + + 1600000-1999999 + 3 + + + 2000000-5999999 + 2 + + + 6000000-8199999 + 3 + + + 8200000-8999999 + 2 + + + 9000000-9999999 + 3 + + + + + 978-99977 + Rwanda + + + 0000000-1999999 + 1 + + + 2000000-3999999 + 0 + + + 4000000-6999999 + 2 + + + 7000000-7999999 + 3 + + + 8000000-9749999 + 0 + + + 9750000-9999999 + 3 + + + + + 978-99978 + Mongolia + + + 0000000-4999999 + 1 + + + 5000000-6999999 + 2 + + + 7000000-9999999 + 3 + + + + + 978-99979 + Honduras + + + 0000000-3999999 + 1 + + + 4000000-7999999 + 2 + + + 8000000-9999999 + 3 + + + + + 978-99980 + Bhutan + + + 0000000-0999999 + 1 + + + 1000000-2999999 + 0 + + + 3000000-5999999 + 2 + + + 6000000-7499999 + 0 + + + 7500000-9999999 + 3 + + + + + 978-99981 + Macau + + + 0000000-1999999 + 1 + + + 2000000-2699999 + 0 + + + 2700000-7499999 + 2 + + + 7500000-9999999 + 3 + + + + + 978-99982 + Benin + + + 0000000-1999999 + 1 + + + 2000000-4999999 + 0 + + + 5000000-6899999 + 2 + + + 6900000-8999999 + 0 + + + 9000000-9999999 + 3 + + + + + 978-99983 + El Salvador + + + 0000000-0999999 + 1 + + + 1000000-4999999 + 0 + + + 5000000-6999999 + 2 + + + 7000000-9499999 + 0 + + + 9500000-9999999 + 3 + + + + + 978-99984 + Brunei Darussalam + + + 0000000-0999999 + 1 + + + 1000000-4999999 + 0 + + + 5000000-6999999 + 2 + + + 7000000-9499999 + 0 + + + 9500000-9999999 + 3 + + + + + 978-99985 + Tajikistan + + + 0000000-1999999 + 1 + + + 2000000-3499999 + 0 + + + 3500000-7999999 + 2 + + + 8000000-8499999 + 0 + + + 8500000-9999999 + 3 + + + + + 978-99986 + Myanmar + + + 0000000-0999999 + 1 + + + 1000000-4999999 + 0 + + + 5000000-6999999 + 2 + + + 7000000-9499999 + 0 + + + 9500000-9999999 + 3 + + + + + 978-99987 + Luxembourg + + + 0000000-6999999 + 0 + + + 7000000-9999999 + 3 + + + + + 978-99988 + Sudan + + + 0000000-0999999 + 1 + + + 1000000-4999999 + 0 + + + 5000000-5499999 + 2 + + + 5500000-7999999 + 0 + + + 8000000-8249999 + 3 + + + 8250000-9999999 + 0 + + + + + 978-99989 + Paraguay + + + 0000000-0999999 + 1 + + + 1000000-4999999 + 0 + + + 5000000-6499999 + 2 + + + 6500000-8999999 + 0 + + + 9000000-9999999 + 3 + + + + + 978-99990 + Ethiopia + + + 0000000-0999999 + 1 + + + 1000000-4999999 + 0 + + + 5000000-5499999 + 2 + + + 5500000-9749999 + 0 + + + 9750000-9999999 + 3 + + + + + 978-99992 + Oman + + + 0000000-1999999 + 1 + + + 2000000-4999999 + 0 + + + 5000000-6499999 + 2 + + + 6500000-9499999 + 0 + + + 9500000-9999999 + 3 + + + + + 978-99993 + Mauritius + + + 0000000-0999999 + 1 + + + 1000000-4999999 + 0 + + + 5000000-5499999 + 2 + + + 5500000-9799999 + 0 + + + 9800000-9999999 + 3 + + + + + 979-10 + France + + + 0000000-1999999 + 2 + + + 2000000-6999999 + 3 + + + 7000000-8999999 + 4 + + + 9000000-9759999 + 5 + + + 9760000-9999999 + 6 + + + + + 979-11 + Korea, Republic + + + 0000000-2499999 + 2 + + + 2500000-5499999 + 3 + + + 5500000-8499999 + 4 + + + 8500000-9499999 + 5 + + + 9500000-9999999 + 6 + + + + + 979-12 + Italy + + + 0000000-1999999 + 0 + + + 2000000-2999999 + 3 + + + 3000000-5449999 + 0 + + + 5450000-5999999 + 4 + + + 6000000-7999999 + 0 + + + 8000000-8499999 + 5 + + + 8500000-9999999 + 0 + + + + + 979-8 + United States + + + 0000000-1999999 + 0 + + + 2000000-2299999 + 3 + + + 2300000-3499999 + 0 + + + 3500000-3999999 + 4 + + + 4000000-8499999 + 4 + + + 8500000-8849999 + 4 + + + 8850000-8999999 + 5 + + + 9000000-9849999 + 0 + + + 9850000-9899999 + 7 + + + 9900000-9999999 + 0 + + + + + diff --git a/bookwyrm/isbn/__init__.py b/bookwyrm/isbn/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/bookwyrm/isbn/isbn.py b/bookwyrm/isbn/isbn.py new file mode 100644 index 000000000..e07d2100d --- /dev/null +++ b/bookwyrm/isbn/isbn.py @@ -0,0 +1,78 @@ +""" Use the range message from isbn-international to hyphenate ISBNs """ +import os +from xml.etree import ElementTree +import requests + +from bookwyrm import settings + + +class IsbnHyphenator: + """Class to manage the range message xml file and use it to hyphenate ISBNs""" + + __range_message_url = "https://www.isbn-international.org/export_rangemessage.xml" + __range_file_path = os.path.join( + settings.BASE_DIR, "bookwyrm", "isbn", "RangeMessage.xml" + ) + __element_tree = None + + def update_range_message(self): + """Download the range message xml file and save it locally""" + response = requests.get(self.__range_message_url) + with open(self.__range_file_path, "w", encoding="utf-8") as file: + file.write(response.text) + self.__element_tree = None + + def hyphenate(self, isbn_13): + """hyphenate the given ISBN-13 number using the range message""" + if isbn_13 is None: + return None + if self.__element_tree is None: + self.__element_tree = ElementTree.parse(self.__range_file_path) + gs1_prefix = isbn_13[:3] + reg_group = self.__find_reg_group(isbn_13, gs1_prefix) + if reg_group is None: + return isbn_13 # failed to hyphenate + registrant = self.__find_registrant(isbn_13, gs1_prefix, reg_group) + if registrant is None: + return isbn_13 # failed to hyphenate + publication = isbn_13[len(gs1_prefix) + len(reg_group) + len(registrant) : -1] + check_digit = isbn_13[-1:] + return "-".join((gs1_prefix, reg_group, registrant, publication, check_digit)) + + def __find_reg_group(self, isbn_13, gs1_prefix): + for ean_ucc_el in self.__element_tree.find("EAN.UCCPrefixes").findall( + "EAN.UCC" + ): + if ean_ucc_el.find("Prefix").text == gs1_prefix: + for rule_el in ean_ucc_el.find("Rules").findall("Rule"): + length = int(rule_el.find("Length").text) + if length == 0: + continue + reg_grp_range = [ + int(x[:length]) for x in rule_el.find("Range").text.split("-") + ] + reg_group = isbn_13[len(gs1_prefix) : len(gs1_prefix) + length] + if reg_grp_range[0] <= int(reg_group) <= reg_grp_range[1]: + return reg_group + return None + return None + + def __find_registrant(self, isbn_13, gs1_prefix, reg_group): + from_ind = len(gs1_prefix) + len(reg_group) + for group_el in self.__element_tree.find("RegistrationGroups").findall("Group"): + if group_el.find("Prefix").text == "-".join((gs1_prefix, reg_group)): + for rule_el in group_el.find("Rules").findall("Rule"): + length = int(rule_el.find("Length").text) + if length == 0: + continue + registrant_range = [ + int(x[:length]) for x in rule_el.find("Range").text.split("-") + ] + registrant = isbn_13[from_ind : from_ind + length] + if registrant_range[0] <= int(registrant) <= registrant_range[1]: + return registrant + return None + return None + + +hyphenator_singleton = IsbnHyphenator() diff --git a/bookwyrm/management/commands/repair_editions.py b/bookwyrm/management/commands/repair_editions.py new file mode 100644 index 000000000..304cd5e51 --- /dev/null +++ b/bookwyrm/management/commands/repair_editions.py @@ -0,0 +1,21 @@ +""" Repair editions with missing works """ +from django.core.management.base import BaseCommand +from bookwyrm import models + + +class Command(BaseCommand): + """command-line options""" + + help = "Repairs an edition that is in a broken state" + + # pylint: disable=unused-argument + def handle(self, *args, **options): + """Find and repair broken editions""" + # Find broken editions + editions = models.Edition.objects.filter(parent_work__isnull=True) + self.stdout.write(f"Repairing {editions.count()} edition(s):") + + # Do repair + for edition in editions: + edition.repair() + self.stdout.write(".", ending="") diff --git a/bookwyrm/migrations/0179_reportcomment_comment_type.py b/bookwyrm/migrations/0179_reportcomment_comment_type.py new file mode 100644 index 000000000..a8a446096 --- /dev/null +++ b/bookwyrm/migrations/0179_reportcomment_comment_type.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.18 on 2023-05-16 16:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0178_auto_20230328_2132"), + ] + + operations = [ + migrations.AddField( + model_name="reportcomment", + name="action_type", + field=models.CharField( + choices=[ + ("comment", "Comment"), + ("resolve", "Resolved report"), + ("reopen", "Re-opened report"), + ("message_reporter", "Messaged reporter"), + ("message_offender", "Messaged reported user"), + ("user_suspension", "Suspended user"), + ("user_unsuspension", "Un-suspended user"), + ("user_perms", "Changed user permission level"), + ("user_deletion", "Deleted user account"), + ("block_domain", "Blocked domain"), + ("approve_domain", "Approved domain"), + ("delete_item", "Deleted item"), + ], + default="comment", + max_length=20, + ), + ), + migrations.RenameModel("ReportComment", "ReportAction"), + ] diff --git a/bookwyrm/migrations/0180_alter_reportaction_options.py b/bookwyrm/migrations/0180_alter_reportaction_options.py new file mode 100644 index 000000000..2979d266e --- /dev/null +++ b/bookwyrm/migrations/0180_alter_reportaction_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.18 on 2023-06-21 22:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0179_reportcomment_comment_type"), + ] + + operations = [ + migrations.AlterModelOptions( + name="reportaction", + options={"ordering": ("created_date",)}, + ), + ] diff --git a/bookwyrm/migrations/0180_alter_user_preferred_language.py b/bookwyrm/migrations/0180_alter_user_preferred_language.py new file mode 100644 index 000000000..b4ab996ec --- /dev/null +++ b/bookwyrm/migrations/0180_alter_user_preferred_language.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.19 on 2023-07-23 19:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0179_populate_sort_title"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="preferred_language", + field=models.CharField( + blank=True, + choices=[ + ("en-us", "English"), + ("ca-es", "Català (Catalan)"), + ("de-de", "Deutsch (German)"), + ("eo-uy", "Esperanto (Esperanto)"), + ("es-es", "Español (Spanish)"), + ("eu-es", "Euskara (Basque)"), + ("gl-es", "Galego (Galician)"), + ("it-it", "Italiano (Italian)"), + ("fi-fi", "Suomi (Finnish)"), + ("fr-fr", "Français (French)"), + ("lt-lt", "Lietuvių (Lithuanian)"), + ("nl-nl", "Nederlands (Dutch)"), + ("no-no", "Norsk (Norwegian)"), + ("pl-pl", "Polski (Polish)"), + ("pt-br", "Português do Brasil (Brazilian Portuguese)"), + ("pt-pt", "Português Europeu (European Portuguese)"), + ("ro-ro", "Română (Romanian)"), + ("sv-se", "Svenska (Swedish)"), + ("zh-hans", "简体中文 (Simplified Chinese)"), + ("zh-hant", "繁體中文 (Traditional Chinese)"), + ], + max_length=255, + null=True, + ), + ), + ] diff --git a/bookwyrm/migrations/0181_merge_20230806_2302.py b/bookwyrm/migrations/0181_merge_20230806_2302.py new file mode 100644 index 000000000..f4f05b886 --- /dev/null +++ b/bookwyrm/migrations/0181_merge_20230806_2302.py @@ -0,0 +1,13 @@ +# Generated by Django 3.2.20 on 2023-08-06 23:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0180_alter_reportaction_options"), + ("bookwyrm", "0180_alter_user_preferred_language"), + ] + + operations = [] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index f5b72f3e4..7b779190b 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -20,7 +20,7 @@ from .readthrough import ReadThrough, ProgressUpdate, ProgressMode from .user import User, KeyPair from .annual_goal import AnnualGoal from .relationship import UserFollows, UserFollowRequest, UserBlocks -from .report import Report, ReportComment +from .report import Report, ReportAction from .federated_server import FederatedServer from .group import Group, GroupMember, GroupMemberInvitation diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index 4b53c6e87..36317ad4e 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -6,8 +6,9 @@ from functools import reduce import json import operator import logging -from typing import List +from typing import Any, Optional from uuid import uuid4 +from typing_extensions import Self import aiohttp from Crypto.PublicKey import RSA @@ -85,7 +86,7 @@ class ActivitypubMixin: super().__init__(*args, **kwargs) @classmethod - def find_existing_by_remote_id(cls, remote_id): + def find_existing_by_remote_id(cls, remote_id: str) -> Self: """look up a remote id in the db""" return cls.find_existing({"id": remote_id}) @@ -137,7 +138,7 @@ class ActivitypubMixin: queue=queue, ) - def get_recipients(self, software=None) -> List[str]: + def get_recipients(self, software=None) -> list[str]: """figure out which inbox urls to post to""" # first we have to figure out who should receive this activity privacy = self.privacy if hasattr(self, "privacy") else "public" @@ -198,7 +199,14 @@ class ActivitypubMixin: class ObjectMixin(ActivitypubMixin): """add this mixin for object models that are AP serializable""" - def save(self, *args, created=None, software=None, priority=BROADCAST, **kwargs): + def save( + self, + *args: Any, + created: Optional[bool] = None, + software: Any = None, + priority: str = BROADCAST, + **kwargs: Any, + ) -> None: """broadcast created/updated/deleted objects as appropriate""" broadcast = kwargs.get("broadcast", True) # this bonus kwarg would cause an error in the base save method @@ -507,14 +515,14 @@ def unfurl_related_field(related_field, sort_field=None): @app.task(queue=BROADCAST) -def broadcast_task(sender_id: int, activity: str, recipients: List[str]): +def broadcast_task(sender_id: int, activity: str, recipients: list[str]): """the celery task for broadcast""" user_model = apps.get_model("bookwyrm.User", require_ready=True) sender = user_model.objects.select_related("key_pair").get(id=sender_id) asyncio.run(async_broadcast(recipients, sender, activity)) -async def async_broadcast(recipients: List[str], sender, data: str): +async def async_broadcast(recipients: list[str], sender, data: str): """Send all the broadcasts simultaneously""" timeout = aiohttp.ClientTimeout(total=10) async with aiohttp.ClientSession(timeout=timeout) as session: diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index c25f8fee2..8cb47e5c8 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -1,6 +1,7 @@ """ database schema for books and shelves """ from itertools import chain import re +from typing import Any from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.indexes import GinIndex @@ -14,6 +15,7 @@ from model_utils.managers import InheritanceManager from imagekit.models import ImageSpecField from bookwyrm import activitypub +from bookwyrm.isbn.isbn import hyphenator_singleton as hyphenator from bookwyrm.preview_images import generate_edition_preview_image_task from bookwyrm.settings import ( DOMAIN, @@ -90,7 +92,7 @@ class BookDataModel(ObjectMixin, BookWyrmModel): abstract = True - def save(self, *args, **kwargs): + def save(self, *args: Any, **kwargs: Any) -> None: """ensure that the remote_id is within this instance""" if self.id: self.remote_id = self.get_remote_id() @@ -204,7 +206,7 @@ class Book(BookDataModel): text += f" ({self.edition_info})" return text - def save(self, *args, **kwargs): + def save(self, *args: Any, **kwargs: Any) -> None: """can't be abstract for query reasons, but you shouldn't USE it""" if not isinstance(self, Edition) and not isinstance(self, Work): raise ValueError("Books should be added as Editions or Works") @@ -320,6 +322,11 @@ class Edition(Book): serialize_reverse_fields = [("file_links", "fileLinks", "-created_date")] deserialize_reverse_fields = [("file_links", "fileLinks")] + @property + def hyphenated_isbn13(self): + """generate the hyphenated version of the ISBN-13""" + return hyphenator.hyphenate(self.isbn_13) + def get_rank(self): """calculate how complete the data is on this edition""" rank = 0 @@ -343,7 +350,7 @@ class Edition(Book): # max rank is 9 return rank - def save(self, *args, **kwargs): + def save(self, *args: Any, **kwargs: Any) -> None: """set some fields on the edition object""" # calculate isbn 10/13 if self.isbn_13 and self.isbn_13[:3] == "978" and not self.isbn_10: @@ -380,6 +387,19 @@ class Edition(Book): return super().save(*args, **kwargs) + @transaction.atomic + def repair(self): + """If an edition is in a bad state (missing a work), let's fix that""" + # made sure it actually NEEDS reapir + if self.parent_work: + return + + new_work = Work.objects.create(title=self.title) + new_work.authors.set(self.authors.all()) + + self.parent_work = new_work + self.save(update_fields=["parent_work"], broadcast=False) + @classmethod def viewer_aware_objects(cls, viewer): """annotate a book query with metadata related to the user""" diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index eb03d457e..e1081ed45 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -61,7 +61,7 @@ class FederatedServer(BookWyrmModel): ).update(active=True, deactivation_reason=None) @classmethod - def is_blocked(cls, url): + def is_blocked(cls, url: str) -> bool: """look up if a domain is blocked""" url = urlparse(url) domain = url.netloc diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 3fe035f58..d21c9363d 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -368,10 +368,16 @@ class TagField(ManyToManyField): activity_type = item.__class__.__name__ if activity_type == "User": activity_type = "Mention" + + if activity_type == "Hashtag": + name = item.name + else: + name = f"@{getattr(item, item.name_field)}" + tags.append( activitypub.Link( href=item.remote_id, - name=f"@{getattr(item, item.name_field)}", + name=name, type=activity_type, ) ) diff --git a/bookwyrm/models/report.py b/bookwyrm/models/report.py index f6e665053..74a9bbe41 100644 --- a/bookwyrm/models/report.py +++ b/bookwyrm/models/report.py @@ -1,11 +1,27 @@ """ flagged for moderation """ from django.core.exceptions import PermissionDenied from django.db import models +from django.utils.translation import gettext_lazy as _ from bookwyrm.settings import DOMAIN from .base_model import BookWyrmModel +# Report action enums +COMMENT = "comment" +RESOLVE = "resolve" +REOPEN = "reopen" +MESSAGE_REPORTER = "message_reporter" +MESSAGE_OFFENDER = "message_offender" +USER_SUSPENSION = "user_suspension" +USER_UNSUSPENSION = "user_unsuspension" +USER_DELETION = "user_deletion" +USER_PERMS = "user_perms" +BLOCK_DOMAIN = "block_domain" +APPROVE_DOMAIN = "approve_domain" +DELETE_ITEM = "delete_item" + + class Report(BookWyrmModel): """reported status or user""" @@ -32,20 +48,65 @@ class Report(BookWyrmModel): def get_remote_id(self): return f"https://{DOMAIN}/settings/reports/{self.id}" + def comment(self, user, note): + """comment on a report""" + ReportAction.objects.create( + action_type=COMMENT, user=user, note=note, report=self + ) + + def resolve(self, user): + """Mark a report as complete""" + self.resolved = True + self.save() + ReportAction.objects.create(action_type=RESOLVE, user=user, report=self) + + def reopen(self, user): + """Wait! This report isn't complete after all""" + self.resolved = False + self.save() + ReportAction.objects.create(action_type=REOPEN, user=user, report=self) + + @classmethod + def record_action(cls, report_id: int, action: str, user): + """Note that someone did something""" + if not report_id: + return + report = cls.objects.get(id=report_id) + ReportAction.objects.create(action_type=action, user=user, report=report) + class Meta: """set order by default""" ordering = ("-created_date",) -class ReportComment(BookWyrmModel): +ReportActionTypes = [ + (COMMENT, _("Comment")), + (RESOLVE, _("Resolved report")), + (REOPEN, _("Re-opened report")), + (MESSAGE_REPORTER, _("Messaged reporter")), + (MESSAGE_OFFENDER, _("Messaged reported user")), + (USER_SUSPENSION, _("Suspended user")), + (USER_UNSUSPENSION, _("Un-suspended user")), + (USER_PERMS, _("Changed user permission level")), + (USER_DELETION, _("Deleted user account")), + (BLOCK_DOMAIN, _("Blocked domain")), + (APPROVE_DOMAIN, _("Approved domain")), + (DELETE_ITEM, _("Deleted item")), +] + + +class ReportAction(BookWyrmModel): """updates on a report""" user = models.ForeignKey("User", on_delete=models.PROTECT) + action_type = models.CharField( + max_length=20, blank=False, default="comment", choices=ReportActionTypes + ) note = models.TextField() report = models.ForeignKey(Report, on_delete=models.PROTECT) class Meta: """sort comments""" - ordering = ("-created_date",) + ordering = ("created_date",) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 042cd8cf8..829ddaef7 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -302,6 +302,7 @@ LANGUAGES = [ ("fi-fi", _("Suomi (Finnish)")), ("fr-fr", _("Français (French)")), ("lt-lt", _("Lietuvių (Lithuanian)")), + ("nl-nl", _("Nederlands (Dutch)")), ("no-no", _("Norsk (Norwegian)")), ("pl-pl", _("Polski (Polish)")), ("pt-br", _("Português do Brasil (Brazilian Portuguese)")), diff --git a/bookwyrm/static/css/bookwyrm/components/_copy.scss b/bookwyrm/static/css/bookwyrm/components/_copy.scss index e0c4246e6..7a47c1dba 100644 --- a/bookwyrm/static/css/bookwyrm/components/_copy.scss +++ b/bookwyrm/static/css/bookwyrm/components/_copy.scss @@ -28,3 +28,31 @@ .vertical-copy button { width: 100%; } + +.copy-tooltip { + overflow: visible; + visibility: hidden; + width: 140px; + background-color: #555; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 5px; + position: absolute; + z-index: 1; + margin-left: -30px; + margin-top: -45px; + opacity: 0; + transition: opacity 0.3s; +} + +.copy-tooltip::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + margin-left: -60px; + border-width: 5px; + border-style: solid; + border-color: #555 transparent transparent transparent; + } diff --git a/bookwyrm/static/css/fonts/icomoon.eot b/bookwyrm/static/css/fonts/icomoon.eot index 69628662b..b86375a90 100644 Binary files a/bookwyrm/static/css/fonts/icomoon.eot and b/bookwyrm/static/css/fonts/icomoon.eot differ diff --git a/bookwyrm/static/css/fonts/icomoon.svg b/bookwyrm/static/css/fonts/icomoon.svg index c67c8b225..a3c902a07 100644 --- a/bookwyrm/static/css/fonts/icomoon.svg +++ b/bookwyrm/static/css/fonts/icomoon.svg @@ -39,6 +39,7 @@ + diff --git a/bookwyrm/static/css/fonts/icomoon.ttf b/bookwyrm/static/css/fonts/icomoon.ttf index 12c79d551..edcbd3b75 100644 Binary files a/bookwyrm/static/css/fonts/icomoon.ttf and b/bookwyrm/static/css/fonts/icomoon.ttf differ diff --git a/bookwyrm/static/css/fonts/icomoon.woff b/bookwyrm/static/css/fonts/icomoon.woff index 624b70f33..47d4bffb5 100644 Binary files a/bookwyrm/static/css/fonts/icomoon.woff and b/bookwyrm/static/css/fonts/icomoon.woff differ diff --git a/bookwyrm/static/css/vendor/icons.css b/bookwyrm/static/css/vendor/icons.css index 6477aee5c..b661ce8e7 100644 --- a/bookwyrm/static/css/vendor/icons.css +++ b/bookwyrm/static/css/vendor/icons.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icomoon'; - src: url('../fonts/icomoon.eot?r7jc98'); - src: url('../fonts/icomoon.eot?r7jc98#iefix') format('embedded-opentype'), - url('../fonts/icomoon.ttf?r7jc98') format('truetype'), - url('../fonts/icomoon.woff?r7jc98') format('woff'), - url('../fonts/icomoon.svg?r7jc98#icomoon') format('svg'); + src: url('../fonts/icomoon.eot?nr4nq7'); + src: url('../fonts/icomoon.eot?nr4nq7#iefix') format('embedded-opentype'), + url('../fonts/icomoon.ttf?nr4nq7') format('truetype'), + url('../fonts/icomoon.woff?nr4nq7') format('woff'), + url('../fonts/icomoon.svg?nr4nq7#icomoon') format('svg'); font-weight: normal; font-style: normal; font-display: block; @@ -122,6 +122,9 @@ .icon-graphic-banknote:before { content: "\e920"; } +.icon-copy:before { + content: "\e92c"; +} .icon-search:before { content: "\e986"; } diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index 0c6958f33..67d64688f 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -65,6 +65,9 @@ let BookWyrm = new (class { .querySelectorAll('input[type="file"]') .forEach(bookwyrm.disableIfTooLarge.bind(bookwyrm)); document.querySelectorAll("[data-copytext]").forEach(bookwyrm.copyText.bind(bookwyrm)); + document + .querySelectorAll("[data-copywithtooltip]") + .forEach(bookwyrm.copyWithTooltip.bind(bookwyrm)); document .querySelectorAll(".modal.is-active") .forEach(bookwyrm.handleActiveModal.bind(bookwyrm)); @@ -524,6 +527,21 @@ let BookWyrm = new (class { textareaEl.parentNode.appendChild(copyButtonEl); } + copyWithTooltip(copyButtonEl) { + const text = document.getElementById(copyButtonEl.dataset.contentId).innerHTML; + const tooltipEl = document.getElementById(copyButtonEl.dataset.tooltipId); + + copyButtonEl.addEventListener("click", () => { + navigator.clipboard.writeText(text); + tooltipEl.style.visibility = "visible"; + tooltipEl.style.opacity = 1; + setTimeout(function () { + tooltipEl.style.visibility = "hidden"; + tooltipEl.style.opacity = 0; + }, 3000); + }); + } + /** * Handle the details dropdown component. * diff --git a/bookwyrm/telemetry/open_telemetry.py b/bookwyrm/telemetry/open_telemetry.py index 00b24d4b0..2a0168ff3 100644 --- a/bookwyrm/telemetry/open_telemetry.py +++ b/bookwyrm/telemetry/open_telemetry.py @@ -1,6 +1,6 @@ from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace import TracerProvider, Tracer from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter from bookwyrm import settings @@ -16,19 +16,19 @@ elif settings.OTEL_EXPORTER_OTLP_ENDPOINT: ) -def instrumentDjango(): +def instrumentDjango() -> None: from opentelemetry.instrumentation.django import DjangoInstrumentor DjangoInstrumentor().instrument() -def instrumentPostgres(): +def instrumentPostgres() -> None: from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor Psycopg2Instrumentor().instrument() -def instrumentCelery(): +def instrumentCelery() -> None: from opentelemetry.instrumentation.celery import CeleryInstrumentor from celery.signals import worker_process_init @@ -37,5 +37,5 @@ def instrumentCelery(): CeleryInstrumentor().instrument() -def tracer(): +def tracer() -> Tracer: return trace.get_tracer(__name__) diff --git a/bookwyrm/templates/book/book_identifiers.html b/bookwyrm/templates/book/book_identifiers.html index ff5aad0bb..d976f72c2 100644 --- a/bookwyrm/templates/book/book_identifiers.html +++ b/bookwyrm/templates/book/book_identifiers.html @@ -4,42 +4,50 @@ {% if book.isbn_13 or book.oclc_number or book.asin or book.aasin or book.isfdb %}
{% if book.isbn_13 %} -
+
{% trans "ISBN:" %}
-
{{ book.isbn_13 }}
+
{{ book.hyphenated_isbn13 }}
+
+ + {% trans "Copied ISBN!" %} +
{% endif %} {% if book.oclc_number %} -
+
{% trans "OCLC Number:" %}
{{ book.oclc_number }}
{% endif %} {% if book.asin %} -
+
{% trans "ASIN:" %}
{{ book.asin }}
{% endif %} {% if book.aasin %} -
+
{% trans "Audible ASIN:" %}
{{ book.aasin }}
{% endif %} {% if book.isfdb %} -
+
{% trans "ISFDB ID:" %}
{{ book.isfdb }}
{% endif %} {% if book.goodreads_key %} -
+
{% trans "Goodreads:" %}
{{ book.goodreads_key }}
diff --git a/bookwyrm/templates/book/edit/edit_book.html b/bookwyrm/templates/book/edit/edit_book.html index d4ca2165d..05f40523f 100644 --- a/bookwyrm/templates/book/edit/edit_book.html +++ b/bookwyrm/templates/book/edit/edit_book.html @@ -111,11 +111,11 @@ {% endif %} {% endfor %}
- {% else %} + {% elif add_author %}

{% blocktrans with name=add_author %}Creating a new author: {{ name }}{% endblocktrans %}

{% endif %} - {% if not book %} + {% if not book.parent_work %}
diff --git a/bookwyrm/templates/book/edit/edit_book_form.html b/bookwyrm/templates/book/edit/edit_book_form.html index 72d80e9cf..23cc6d097 100644 --- a/bookwyrm/templates/book/edit/edit_book_form.html +++ b/bookwyrm/templates/book/edit/edit_book_form.html @@ -10,7 +10,9 @@ {% csrf_token %} +{% if form.parent_work %} +{% endif %}
diff --git a/bookwyrm/templates/book/editions/editions.html b/bookwyrm/templates/book/editions/editions.html index 16f65e459..aa2b68bdb 100644 --- a/bookwyrm/templates/book/editions/editions.html +++ b/bookwyrm/templates/book/editions/editions.html @@ -58,6 +58,7 @@
{% csrf_token %} {{ work_form.title }} + {{ work_form.sort_title }} {{ work_form.subtitle }} {{ work_form.authors }} {{ work_form.description }} diff --git a/bookwyrm/templates/book/editions/search_filter.html b/bookwyrm/templates/book/editions/search_filter.html index 91c76422d..d4cba6baf 100644 --- a/bookwyrm/templates/book/editions/search_filter.html +++ b/bookwyrm/templates/book/editions/search_filter.html @@ -4,7 +4,7 @@ {% block filter %}
- +
{% endblock %} diff --git a/bookwyrm/templates/get_started/books.html b/bookwyrm/templates/get_started/books.html index 93196dbcf..47f427a02 100644 --- a/bookwyrm/templates/get_started/books.html +++ b/bookwyrm/templates/get_started/books.html @@ -6,7 +6,7 @@

{% trans "What are you reading?" %}

- + {% if request.GET.query and not book_results %}

{% blocktrans with query=request.GET.query %}No books found for "{{ query }}"{% endblocktrans %}. {% blocktrans %}You can add books when you start using {{ site_name }}.{% endblocktrans %}

{% endif %} diff --git a/bookwyrm/templates/get_started/users.html b/bookwyrm/templates/get_started/users.html index 4f95882f5..136efe378 100644 --- a/bookwyrm/templates/get_started/users.html +++ b/bookwyrm/templates/get_started/users.html @@ -8,7 +8,7 @@

{% trans "You can follow users on other BookWyrm instances and federated services like Mastodon." %}

- + {% if request.GET.query and no_results %}

{% blocktrans with query=request.GET.query %}No users found for "{{ query }}"{% endblocktrans %}

{% endif %} diff --git a/bookwyrm/templates/groups/members.html b/bookwyrm/templates/groups/members.html index 0fa4048fa..ff09b9ec2 100644 --- a/bookwyrm/templates/groups/members.html +++ b/bookwyrm/templates/groups/members.html @@ -8,7 +8,7 @@
- +
-
+
- {% endfor %} -
- {% csrf_token %} -
- - -
-
- -
-
{% endblock %} diff --git a/bookwyrm/templates/settings/reports/report_links_table.html b/bookwyrm/templates/settings/reports/report_links_table.html index b0ebf73a5..cc9f2f7f2 100644 --- a/bookwyrm/templates/settings/reports/report_links_table.html +++ b/bookwyrm/templates/settings/reports/report_links_table.html @@ -13,8 +13,18 @@ -
+ {% if link.domain.status != "approved" %} + + {% csrf_token %} + +
+ {% endif %} + + {% if link.domain.status != "blocked" %} +
+ {% csrf_token %}
+ {% endif %} {% endblock %} diff --git a/bookwyrm/templates/settings/users/delete_user_form.html b/bookwyrm/templates/settings/users/delete_user_form.html index eeb5e39fb..267e26ae9 100644 --- a/bookwyrm/templates/settings/users/delete_user_form.html +++ b/bookwyrm/templates/settings/users/delete_user_form.html @@ -6,7 +6,7 @@ {% endblock %} {% block form %} -
+ {% csrf_token %}

{% blocktrans trimmed with username=user.localname %} diff --git a/bookwyrm/templates/settings/users/user_moderation_actions.html b/bookwyrm/templates/settings/users/user_moderation_actions.html index 5bd007e0d..4a624a5e4 100644 --- a/bookwyrm/templates/settings/users/user_moderation_actions.html +++ b/bookwyrm/templates/settings/users/user_moderation_actions.html @@ -22,12 +22,12 @@

{% endif %} {% if user.is_active or user.deactivation_reason == "pending" %} -
+ {% csrf_token %}
{% else %} -
+ {% csrf_token %}
@@ -49,7 +49,7 @@ {% if user.local %}
-
+ {% csrf_token %} {% if group_form.non_field_errors %} diff --git a/bookwyrm/templates/snippets/create_status/post_options_block.html b/bookwyrm/templates/snippets/create_status/post_options_block.html index 652f8adbd..9627ac9cd 100644 --- a/bookwyrm/templates/snippets/create_status/post_options_block.html +++ b/bookwyrm/templates/snippets/create_status/post_options_block.html @@ -15,7 +15,11 @@
diff --git a/bookwyrm/templates/snippets/status/layout.html b/bookwyrm/templates/snippets/status/layout.html index 4e5b75cc0..19bbd4e1e 100644 --- a/bookwyrm/templates/snippets/status/layout.html +++ b/bookwyrm/templates/snippets/status/layout.html @@ -18,7 +18,7 @@