├── tests ├── __init__.py ├── test_random_parameters.py └── test_search_live.py ├── setup.cfg ├── derpibooru ├── __init__.py ├── user.py ├── comment.py ├── sort.py ├── helpers.py ├── request.py ├── image.py ├── query.py └── search.py ├── setup.py ├── LICENSE.txt └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /tests/test_random_parameters.py: -------------------------------------------------------------------------------- 1 | from derpibooru import sort 2 | 3 | def test_sorting_methods(): 4 | sorting_methods = { 5 | "created_at", 6 | "score", 7 | "relevance", 8 | "height", 9 | "comments", 10 | "random" 11 | } 12 | 13 | methods = sort.__dict__ 14 | 15 | assert len(sorting_methods) == len(methods) 16 | assert sorting_methods == sort.methods 17 | 18 | for key, value in methods.items(): 19 | assert key == value.upper() 20 | -------------------------------------------------------------------------------- /derpibooru/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Derpibooru API bindings 5 | ~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | Python bindings for Derpibooru's API 8 | 9 | Typical usage: 10 | 11 | >>> from derpibooru import Search, sort 12 | >>> for image in Search().sort_by(sort.SCORE): 13 | ... print(image.url) 14 | 15 | Full API Documentation is found at . 16 | 17 | Library documentation is found at . 18 | 19 | """ 20 | 21 | __title__ = "DerPyBooru" 22 | __version__ = "0.7.5" 23 | __author__ = "Joshua Stone" 24 | __license__ = "Simplified BSD Licence" 25 | __copyright__ = "Copyright (c) 2014, Joshua Stone" 26 | 27 | from .search import Search 28 | from .query import query 29 | from .sort import sort 30 | from .user import user 31 | 32 | __all__ = [ 33 | "Search", 34 | "query", 35 | "sort", 36 | "user" 37 | ] 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | from setuptools import find_packages 5 | 6 | setup( 7 | name = "DerPyBooru", 8 | description = "Python bindings for Derpibooru's API", 9 | url = "https://github.com/joshua-stone/DerPyBooru", 10 | version = "0.7.5", 11 | author = "Joshua Stone", 12 | author_email = "joshua.gage.stone@gmail.com", 13 | license = "Simplified BSD License", 14 | platforms = ["any"], 15 | packages = find_packages(), 16 | install_requires = ["requests"], 17 | include_package_data = True, 18 | download_url = "https://github.com/joshua-stone/DerPyBooru/tarball/0.7.2", 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "Intended Audience :: Developers", 22 | "Operating System :: OS Independent", 23 | "License :: OSI Approved :: BSD License", 24 | "Topic :: Software Development :: Libraries :: Python Modules", 25 | "Programming Language :: Python :: 2.7", 26 | "Programming Language :: Python :: 3" 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Joshua Stone 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /tests/test_search_live.py: -------------------------------------------------------------------------------- 1 | from derpibooru import Search 2 | 3 | def test_query(): 4 | """ 5 | Tests whether the results in a query contain the tag that was being searched 6 | for 7 | """ 8 | limit, tag = 10, "sunset shimmer" 9 | images = [image for image in Search().query(tag).limit(limit)] 10 | 11 | assert len(images) == limit 12 | 13 | for image in images: 14 | assert tag in image.tags 15 | 16 | def test_ascending(): 17 | """ 18 | Tests whether ascending search is in the correct order 19 | """ 20 | limit = 10 21 | images = [image for image in Search().ascending().limit(limit)] 22 | 23 | assert len(images) == limit 24 | 25 | for image in images: 26 | # Check if the images are in ascending order 27 | # by comparing the ID of the next image 28 | if image is not images[-1]: 29 | next_image = images[images.index(image) + 1] 30 | assert image.id_number < next_image.id_number 31 | 32 | def test_descending(): 33 | """ 34 | Tests whether descending search is in the correct order 35 | """ 36 | limit = 10 37 | images = [image for image in Search().descending().limit(limit)] 38 | 39 | assert len(images) == limit 40 | 41 | for image in images: 42 | # Check if the image IDs are listed in descending order 43 | # by comparing the ID of the next image 44 | if image is not images[-1]: 45 | next_image = images[images.index(image) + 1] 46 | assert image.id_number > next_image.id_number 47 | 48 | -------------------------------------------------------------------------------- /derpibooru/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014, Joshua Stone 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # * Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | __all__ = [ 28 | "user" 29 | ] 30 | 31 | class User(object): 32 | def __init__(self): 33 | for option in self.options: 34 | setattr(self, option.upper(), option) 35 | 36 | @property 37 | def options(self): 38 | available_options = { 39 | "only", 40 | "not" 41 | } 42 | return available_options 43 | 44 | user = User() 45 | 46 | -------------------------------------------------------------------------------- /derpibooru/comment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014, Joshua Stone 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # * Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | __all__ = [ 28 | "Comment" 29 | ] 30 | 31 | class Comment(object): 32 | def __init__(self, data): 33 | self._data = data 34 | for field, body in self.data.items(): 35 | setattr(self, field, body) 36 | 37 | def __str__(self): 38 | return "Comment({0})".format(self.author) 39 | 40 | @property 41 | def data(self): 42 | return self._data 43 | 44 | -------------------------------------------------------------------------------- /derpibooru/sort.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014, Joshua Stone 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # * Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | __all__ = [ 28 | "sort" 29 | ] 30 | 31 | class Sort(object): 32 | def __init__(self): 33 | for method in self.methods: 34 | setattr(self, method.upper(), method) 35 | 36 | @property 37 | def methods(self): 38 | sorting_methods = { 39 | "created_at", 40 | "score", 41 | "relevance", 42 | "height", 43 | "comments", 44 | "random" 45 | } 46 | 47 | return sorting_methods 48 | 49 | sort = Sort() 50 | 51 | -------------------------------------------------------------------------------- /derpibooru/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014, Joshua Stone 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # * Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | __all__ = [ 28 | "tags", 29 | "api_key", 30 | "sort_format", 31 | "format_params", 32 | "join_params" 33 | ] 34 | 35 | from .sort import sort 36 | from .user import user 37 | 38 | def tags(q): 39 | tags = {str(tag).strip() for tag in q if tag} 40 | 41 | return tags if tags else {} 42 | 43 | def api_key(api_key): 44 | return str(api_key) if api_key else "" 45 | 46 | def validate_filter(filter_id): 47 | # is it always an number? 48 | return str(filter_id) if filter_id else "" 49 | 50 | def sort_format(sf): 51 | if sf not in sort.methods: 52 | raise AttributeError(sf) 53 | else: 54 | return sf 55 | 56 | def user_option(option): 57 | if option: 58 | if option not in user.options: 59 | raise AttributeError(option) 60 | else: 61 | return option 62 | else: 63 | return "" 64 | 65 | def format_params(params): 66 | p = {} 67 | 68 | for key, value in params.items(): 69 | if key == "key" or key == "filter_id": 70 | if value: 71 | p[key] = value 72 | elif key in ("faves", "upvotes", "uploads", "watched"): 73 | if value and params["key"]: 74 | p[key] = value 75 | elif key == "q": 76 | p["q"] = ",".join(value) if value else "*" 77 | else: 78 | p[key] = value 79 | 80 | return p 81 | 82 | def join_params(old_params, new_params): 83 | new_dict = dict(list(old_params.items()) + list(new_params.items())) 84 | 85 | return new_dict 86 | 87 | def set_limit(limit): 88 | 89 | if limit is not None: 90 | l = int(limit) 91 | else: 92 | l = None 93 | 94 | return l 95 | 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DerPyBooru 2 | 3 | Python bindings for Derpibooru's API 4 | 5 | License: **Simplified BSD License** 6 | 7 | Version: **0.7.5** 8 | 9 | ## Features 10 | 11 | - High-level abstraction over Derpibooru's REST API 12 | - Parameter chaining for ease of manipulation 13 | - Syntactic sugar for queries, e.g., "query.score >= 100" compiling to "score.gte:100" 14 | - Design focusing on iterables and lazy generation for network efficiency 15 | 16 | ## Dependencies 17 | 18 | - python2.7 or newer 19 | - requests 20 | 21 | ## How to install 22 | 23 | ### Python 2.7 24 | 25 | $ pip install derpybooru 26 | 27 | ### Python 3.x 28 | 29 | $ pip3 install derpybooru 30 | 31 | ## Checking documentation 32 | 33 | ### Python 2.7 34 | 35 | $ pydoc derpibooru 36 | 37 | ### Python 3.x 38 | 39 | $ pydoc3 derpibooru 40 | 41 | ## Typical usage 42 | 43 | ### Getting images currently on Derpibooru's front page 44 | 45 | ```python 46 | from derpibooru import Search 47 | 48 | for image in Search(): 49 | id_number, score, tags = image.id, image.score, ", ".join(image.tags) 50 | print("#{} - score: {:>3} - {}".format(id_number, score, tags)) 51 | ``` 52 | 53 | ### Searching posts by tag 54 | 55 | ```python 56 | from derpibooru import Search 57 | 58 | for image in Search().query("rarity", "twilight sparkle"): 59 | print(image.url) 60 | ``` 61 | 62 | ### Crawling Derpibooru from first to last post 63 | 64 | ```python 65 | from derpibooru import Search 66 | 67 | # This is only an example and shouldn't be used in practice as it abuses 68 | # Derpibooru's licensing terms 69 | for image in Search().ascending().limit(None): 70 | id_number, score, tags = image.id, image.score, ", ".join(image.tags) 71 | print("#{} - score: {:>3} - {}".format(id_number, score, tags)) 72 | ``` 73 | 74 | ### Getting random posts 75 | 76 | ```python 77 | from derpibooru import Search, sort 78 | 79 | for post in Search().sort_by(sort.RANDOM): 80 | print(post.url) 81 | ``` 82 | 83 | ### Getting top 100 posts 84 | ```python 85 | from derpibooru import Search, sort 86 | 87 | top_scoring = [post for post in Search().sort_by(sort.SCORE).limit(100)] 88 | ``` 89 | 90 | ### Storing and passing new search parameters 91 | 92 | ```python 93 | from derpibooru import Search, sort 94 | 95 | params = Search().sort_by(sort.SCORE).limit(100).parameters 96 | 97 | top_scoring = Search(**params) 98 | top_animated = top_scoring.query("animated") 99 | ``` 100 | 101 | ### Filtering by metadata 102 | 103 | ```python 104 | from derpibooru import Search, query 105 | 106 | q = { 107 | "wallpaper", 108 | query.width == 1920, 109 | query.height == 1080, 110 | query.score >= 100 111 | } 112 | 113 | wallpapers = [image for image in Search().query(*q)] 114 | ``` 115 | ### Getting the latest images from a watchlist 116 | 117 | ```python 118 | 119 | from derpibooru import Search, user 120 | 121 | key = "your_api_key" 122 | 123 | for post in Search().key(key).watched(user.ONLY): 124 | id_number, score, tags = post.id, post.score, ", ".join(post.tags) 125 | print("#{} - score: {:>3} - {}".format(id_number, score, tags)) 126 | 127 | -------------------------------------------------------------------------------- /derpibooru/request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014, Joshua Stone 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # * Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from requests import get, codes 28 | from sys import version_info 29 | from .helpers import format_params, join_params 30 | 31 | __all__ = [ 32 | "url", 33 | "request", 34 | "get_images", 35 | "get_image_data", 36 | "set_limit" 37 | ] 38 | 39 | if version_info < (3, 0): 40 | from urllib import urlencode 41 | else: 42 | from urllib.parse import urlencode 43 | 44 | def url(params): 45 | p = format_params(params) 46 | url = "https://derpibooru.org/search?{}".format(urlencode(p)) 47 | 48 | return url 49 | 50 | def request(params): 51 | search, p = "https://derpibooru.org/api/v1/json/search/images", format_params(params) 52 | 53 | request = get(search, params=p) 54 | 55 | while request.status_code == codes.ok: 56 | images, image_count = request.json()["images"], 0 57 | for image in images: 58 | yield image 59 | image_count += 1 60 | if image_count < 50: 61 | break 62 | 63 | p["page"] += 1 64 | 65 | request = get(search, params=p) 66 | 67 | def get_images(parameters, limit=50): 68 | params = join_params(parameters, {"per_page": 50, "page": 1}) 69 | 70 | if limit is not None: 71 | l = limit 72 | if l > 0: 73 | r, counter = request(params), 0 74 | for index, image in enumerate(r, start=1): 75 | yield image 76 | if index >= l: 77 | break 78 | else: 79 | r = request(params) 80 | for image in r: 81 | yield image 82 | 83 | def get_image_data(id_number): 84 | url = "https://derpibooru.org/api/v1/json/images/{}".format(id_number) 85 | 86 | request = get(url) 87 | 88 | if request.status_code == codes.ok: 89 | data = request.json() 90 | 91 | if "duplicate_of" in data: 92 | return get_image_data(data["duplicate_of"]) 93 | else: 94 | return data 95 | 96 | -------------------------------------------------------------------------------- /derpibooru/image.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014, Joshua Stone 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # * Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from re import sub 28 | from .request import get_image_data 29 | from .comment import Comment 30 | 31 | __all__ = [ 32 | "Image" 33 | ] 34 | 35 | class Image(object): 36 | """ 37 | This class provides a thin wrapper around JSON data, mapping each value to 38 | its own property. Once instantiated the data is immutable so as to reflect 39 | the stateless nature of a REST API 40 | """ 41 | def __init__(self, data): 42 | self._data = data 43 | 44 | for field, body in data.items(): 45 | if not hasattr(self, field): 46 | setattr(self, field, body) 47 | 48 | def __str__(self): 49 | return "Image({0})".format(self.id) 50 | 51 | @property 52 | def tags(self): 53 | return self.data["tags"] 54 | 55 | @property 56 | def representations(self): 57 | return self.data["representations"] 58 | 59 | @property 60 | def thumb(self): 61 | return self.representations["thumb"] 62 | 63 | @property 64 | def thumb_tiny(self): 65 | return self.representations["thumb_tiny"] 66 | 67 | @property 68 | def small(self): 69 | return self.representations["small"] 70 | 71 | @property 72 | def full(self): 73 | return self.representations["full"] 74 | 75 | @property 76 | def tall(self): 77 | return self.representations["tall"] 78 | 79 | @property 80 | def large(self): 81 | return self.representations["large"] 82 | 83 | @property 84 | def medium(self): 85 | return self.representations["medium"] 86 | 87 | @property 88 | def thumb_small(self): 89 | return self.representations["thumb_small"] 90 | 91 | @property 92 | def image(self): 93 | return self.representations["full"] 94 | 95 | @property 96 | def url(self): 97 | return "https://derpibooru.org/{}".format(self.id) 98 | 99 | @property 100 | def data(self): 101 | return self._data 102 | 103 | def update(self): 104 | data = get_image_data(self.id) 105 | 106 | if data: 107 | self._data = data 108 | 109 | -------------------------------------------------------------------------------- /derpibooru/query.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014, Joshua Stone 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # * Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | __all__ = [ 28 | "query" 29 | ] 30 | 31 | class Query_Field(object): 32 | def __init__(self, name, is_neg=False): 33 | self.name = name 34 | self.is_neg = is_neg 35 | 36 | def __neg__(self): 37 | return self.__class__(self.name, is_neg=True) 38 | 39 | class Equal(Query_Field): 40 | def __eq__(self, value): 41 | if value: 42 | return "{}{}:{}".format( 43 | "-" if self.is_neg else "", 44 | self.name, 45 | value 46 | ) 47 | else: 48 | raise ValueError(value) 49 | 50 | def __gt__(self, value): 51 | raise AttributeError("gt") 52 | 53 | def __lt__(self, value): 54 | raise AttributeError("lt") 55 | 56 | def __ge__(self, value): 57 | raise AttributeError("ge") 58 | 59 | def __le__(self, value): 60 | raise AttributeError("le") 61 | 62 | 63 | class Comparable(Query_Field): 64 | def op(self, op, value): 65 | try: 66 | float(value) 67 | return "{}{}.{}:{}".format( 68 | "-" if self.is_neg else "", 69 | self.name, 70 | op, 71 | value 72 | ) 73 | except: 74 | raise ValueError(value) 75 | 76 | def __eq__(self, value): 77 | return self.op("eq", value) 78 | 79 | def __gt__(self, value): 80 | return self.op("gt", value) 81 | 82 | def __lt__(self, value): 83 | return self.op("lt", value) 84 | 85 | def __ge__(self, value): 86 | return self.op("gte", value) 87 | 88 | def __le__(self, value): 89 | return self.op("lte", value) 90 | 91 | 92 | class Query(object): 93 | def __init__(self): 94 | for field in ["description", "faved_by", "source_url", "orig_sha512_hash", 95 | "sha512_hash", "uploader"]: 96 | setattr(self, field, Equal(field)) 97 | 98 | for field in ["aspect_ratio", "downvotes", "faves", "height", "score", 99 | "upvotes", "width"]: 100 | setattr(self, field, Comparable(field)) 101 | 102 | def __neg__(self): 103 | return self.__class__() 104 | 105 | query = Query() 106 | -------------------------------------------------------------------------------- /derpibooru/search.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014, Joshua Stone 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # 9 | # * Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # * Redistributions in binary form must reproduce the above copyright notice, 13 | # this list of conditions and the following disclaimer in the documentation 14 | # and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | from sys import version_info 28 | 29 | from .request import get_images, url 30 | from .image import Image 31 | from .helpers import tags, api_key, sort_format, join_params, user_option, set_limit, validate_filter 32 | 33 | __all__ = [ 34 | "Search" 35 | ] 36 | 37 | class Search(object): 38 | """ 39 | Search() is the primary interface for interacting with Derpibooru's REST API. 40 | 41 | All properties are read-only, and every method returns a new instance of 42 | Search() to avoid mutating state in ongoing search queries. This makes object 43 | interactions predictable as well as making versioning of searches relatively 44 | easy. 45 | """ 46 | def __init__(self, key="", q={}, sf="created_at", sd="desc", limit=50, 47 | faves="", upvotes="", uploads="", watched="", filter_id=""): 48 | """ 49 | By default initializes an instance of Search with the parameters to get 50 | the first 50 images on Derpibooru's front page. 51 | """ 52 | self._params = { 53 | "key": api_key(key), 54 | "q": tags(q), 55 | "sf": sort_format(sf), 56 | "sd": sd, 57 | "faves": user_option(faves), 58 | "upvotes": user_option(upvotes), 59 | "uploads": user_option(uploads), 60 | "watched": user_option(watched), 61 | "filter_id": validate_filter(filter_id) 62 | } 63 | self._limit = set_limit(limit) 64 | self._search = get_images(self._params, self._limit) 65 | 66 | def __iter__(self): 67 | """ 68 | Make Search() iterable so that new search results can be lazily generated 69 | for performance reasons. 70 | """ 71 | return self 72 | 73 | @property 74 | def parameters(self): 75 | """ 76 | Returns a list of available parameters; useful for passing state to new 77 | instances of Search(). 78 | """ 79 | params = join_params(self._params, {"limit": self._limit}) 80 | return params 81 | 82 | @property 83 | def url(self): 84 | """ 85 | Returns a search URL built on set parameters. Example based on default 86 | parameters: 87 | 88 | https://derpibooru.org/search?sd=desc&sf=created_at&q=%2A 89 | """ 90 | return url(self._params) 91 | 92 | def key(self, key=""): 93 | """ 94 | Takes a user's API key string which applies content settings. API keys can 95 | be found at . 96 | """ 97 | params = join_params(self.parameters, {"key": key}) 98 | 99 | return self.__class__(**params) 100 | 101 | def query(self, *q): 102 | """ 103 | Takes one or more strings for searching by tag and/or metadata. 104 | """ 105 | params = join_params(self.parameters, {"q": q}) 106 | 107 | return self.__class__(**params) 108 | 109 | def sort_by(self, sf): 110 | """ 111 | Determines how to sort search results. Available sorting methods are 112 | sort.SCORE, sort.COMMENTS, sort.HEIGHT, sort.RELEVANCE, sort.CREATED_AT, 113 | and sort.RANDOM; default is sort.CREATED_AT. 114 | """ 115 | params = join_params(self.parameters, {"sf": sf}) 116 | 117 | return self.__class__(**params) 118 | 119 | def descending(self): 120 | """ 121 | Order results from largest to smallest; default is descending order. 122 | """ 123 | params = join_params(self.parameters, {"sd": "desc"}) 124 | 125 | return self.__class__(**params) 126 | 127 | def ascending(self): 128 | """ 129 | Order results from smallest to largest; default is descending order. 130 | """ 131 | params = join_params(self.parameters, {"sd": "asc"}) 132 | 133 | return self.__class__(**params) 134 | 135 | def limit(self, limit): 136 | """ 137 | Set absolute limit on number of images to return, or set to None to return 138 | as many results as needed; default 50 posts. 139 | """ 140 | params = join_params(self.parameters, {"limit": limit}) 141 | 142 | return self.__class__(**params) 143 | 144 | def filter(self, filter_id=""): 145 | """ 146 | Takes a filter's ID to be used in the current search context. Filter IDs can 147 | be found at by inspecting the URL parameters. 148 | 149 | If no filter is provided, the user's current filter will be used. 150 | """ 151 | params = join_params(self.parameters, {"filter_id": filter_id}) 152 | 153 | return self.__class__(**params) 154 | 155 | 156 | def faves(self, option): 157 | """ 158 | Set whether to filter by a user's faves list. Options available are 159 | user.ONLY, user.NOT, and None; default is None. 160 | """ 161 | params = join_params(self.parameters, {"faves": option}) 162 | 163 | return self.__class__(**params) 164 | 165 | def upvotes(self, option): 166 | """ 167 | Set whether to filter by a user's upvoted list. Options available are 168 | user.ONLY, user.NOT, and None; default is None. 169 | """ 170 | params = join_params(self.parameters, {"upvotes": option}) 171 | 172 | return self.__class__(**params) 173 | 174 | def uploads(self, option): 175 | """ 176 | Set whether to filter by a user's uploads list. Options available are 177 | user.ONLY, user.NOT, and None; default is None. 178 | """ 179 | params = join_params(self.parameters, {"uploads": option}) 180 | 181 | return self.__class__(**params) 182 | 183 | def watched(self, option): 184 | """ 185 | Set whether to filter by a user's watchlist. Options available are 186 | user.ONLY, user.NOT, and None; default is None. 187 | """ 188 | params = join_params(self.parameters, {"watched": option}) 189 | 190 | return self.__class__(**params) 191 | 192 | if version_info < (3, 0): 193 | def next(self): 194 | """ 195 | Returns a result wrapped in a new instance of Image(). 196 | """ 197 | return Image(self._search.next()) 198 | 199 | Search.next = next 200 | 201 | else: 202 | def __next__(self): 203 | """ 204 | Returns a result wrapped in a new instance of Image(). 205 | """ 206 | return Image(next(self._search)) 207 | 208 | Search.__next__ = __next__ 209 | 210 | --------------------------------------------------------------------------------