diff --git a/cursepy/__init__.py b/cursepy/__init__.py index 2f649dd..182588b 100644 --- a/cursepy/__init__.py +++ b/cursepy/__init__.py @@ -4,10 +4,10 @@ # Import the necessary components: -from cursepy.wrapper import CurseClient +from cursepy.wrapper import CurseClient, MinecraftWrapper # Define some metadata here: -__version__ = '1.2.0' +__version__ = '1.3.0' __author__ = 'Owen Cochell' diff --git a/cursepy/classes/base.py b/cursepy/classes/base.py index 9f96916..013c282 100644 --- a/cursepy/classes/base.py +++ b/cursepy/classes/base.py @@ -169,7 +169,7 @@ def low_download(self, url: str, path: str=None) -> bytes: # Get the data: - data = url_proto.get_data('') + data = url_proto.get_data(url) # Determine if we should write to a file: @@ -602,14 +602,20 @@ class CurseDependency(BaseCurseInstance): """ CurseDependency - Represents a dependency of an addon. - + A dependency is an addon file that is wanted or required by another addon. This class represents these dependencies. - + We contain useful metadata on each dependency, such as the addon ID and file ID this addon is apart of. We also offer methods to retrieve the addon and file. - + + In some cases, we will not have all the required dependency information + such as the dependency ID and file ID. + This can happen if you use the ForgeSVC handlers and get ALL addon files + instead of requesting a specific one. + If this dependency instance is limited, then 'id' and 'file_id' will be None. + We contain the following parameters: * id - ID of the dependency @@ -650,16 +656,6 @@ def addon(self) -> CurseAddon: return self.hands.addon(self.addon_id) - def file(self) -> CurseFile: - """ - Gets the CurseFile this dependency is apart of. - - :return: CurseFile this dependency is apart of - :rtype: CurseFile - """ - - return self.hands.file(self.addon_id, self.file_id) - @dataclass class CurseAddon(BaseCurseInstance): diff --git a/cursepy/classes/search.py b/cursepy/classes/search.py index 2d00221..43ab733 100644 --- a/cursepy/classes/search.py +++ b/cursepy/classes/search.py @@ -18,7 +18,7 @@ class SearchParam: We define the following values: * filter - Term to search for(i.e, 'Inventory Mods') - * index - Page index of results to view + * index - Addon index to start on * pageSize - Number of items to display per page * gameVersion - Game version to search under * sort - Sorting method to use @@ -34,8 +34,8 @@ class SearchParam: """ searchFilter: Optional[str] = field(default=None) # Term to search for - index: Optional[int] = field(default=None) # Page of search results to view - pageSize: Optional[int] = field(default=None) # Number of items to display per page + index: Optional[int] = field(default=0) # Index of addon to start on + pageSize: Optional[int] = field(default=20) # Number of items to display per page gameVersion: Optional[int] = field(default=None) # Game version to use sort: Optional[int] = field(default=None) # Sort method to use @@ -59,6 +59,50 @@ def asdict(self) -> dict: return asdict(self) + def set_page(self, num: int): + """ + Changes the page we are on. + + We change the index to reach the next page, + we use this equation to determine this: + + index = num * pageSize + + This will set the index to the given page. + For example, if we set the page number to two, + have a page size of five, and an index of three, + then the resulting index after the operation will be 10. + + :param page: Page number to set the index to + :type page: int + """ + + self.index = num * self.pageSize + + def bump_page(self, num: int=1): + """ + Bumps the page up or down. + + We add the page change to the current index using this equation: + + index += num * pageSize + + This will change the page in relation with the current index. + For example, if you bump the page up twice, have a page size of five + and an index of three, the resulting index after the operation will be 13. + + You can supply a negative number to bump the page downward. + If the index ends up below zero, then we will set the index to zero. + + :param num: Number of pages to bump + :type num: int + """ + + self.index += num * self.pageSize + + if self.index < 0: + + self.index = 0 def url_convert(search: SearchParam, url: str='') -> str: """ diff --git a/cursepy/handlers/forgesvc.py b/cursepy/handlers/forgesvc.py index 9f73df7..03be669 100644 --- a/cursepy/handlers/forgesvc.py +++ b/cursepy/handlers/forgesvc.py @@ -480,7 +480,7 @@ def format(self, data: dict) -> Tuple[base.CurseFile, ...]: # Convert the file: - final.append(SVCFile.low_format(file, id)) + final.append(SVCFile.low_format(file, id, limited=True)) # Return the data: @@ -527,13 +527,17 @@ def format(self, data: dict) -> base.CurseFile: return self.low_format(data, id) @staticmethod - def low_format(data: dict, addon_id: int) -> base.CurseFile: + def low_format(data: dict, addon_id: int, limited=False) -> base.CurseFile: """ Low-level format method. We are static to allow other classes to use us to format data. The actual 'format()' method will extract the addon ID from the URL and pass it along to us. + + In some cases, we are not given full dependence info, + for example when all files for a given addon is requested. + You can use the 'limited' parameter to prevent a key error. :param data: Data to be formatted :type data: dict @@ -549,6 +553,12 @@ def low_format(data: dict, addon_id: int) -> base.CurseFile: for depen in data['dependencies']: + if limited: + + final.append(base.CurseDependency(None, depen['addonId'], None, depen['type'])) + + continue + # Add the dependency ID's: final.append(base.CurseDependency(depen['id'], depen['addonId'], depen['fileId'], depen['type'])) diff --git a/cursepy/wrapper.py b/cursepy/wrapper.py index f56f67d..23a9fdc 100644 --- a/cursepy/wrapper.py +++ b/cursepy/wrapper.py @@ -167,7 +167,7 @@ def iter_search(self, game_id: int, category_id: int, search: SearchParam=None) # No results! Raise 'StopIteration'! - raise StopIteration() + break # Iterate over the result: @@ -179,7 +179,7 @@ def iter_search(self, game_id: int, category_id: int, search: SearchParam=None) # Bump the index and continue: - search.index =+ 1 + search.bump_page() def addon(self, addon_id: int) -> base.CurseAddon: """ @@ -324,6 +324,7 @@ class MinecraftWrapper(CurseClient): MODPACKS = 4471 MODS = 6 WORLDS = 17 + BUKKIT = 5 def search_resource_packs(self, search: SearchParam=None) -> Tuple[base.CurseAddon, ...]: """ @@ -377,10 +378,25 @@ def search_worlds(self, search: SearchParam=None) -> Tuple[base.CurseAddon, ...] :param search: SearchParam to use, defaults to None :type search: SearchParam, optional - :return: Tuple of CurseAddons, optional + :return: Tuple of CurseAddons :rtype: Tuple[base.CurseAddon, ...] """ # Search the worlds: return self.search(MinecraftWrapper.GAME_ID, MinecraftWrapper.WORLDS, search) + + def search_plugins(self, search: SearchParam=None) -> Tuple[base.CurseAddon]: + """ + Searches the Plugins category for addons. + Again, we use the SearchParam for the search operation. + + :param search: SearchParam to use, defaults to None + :type search: SearchParam, optional + :return: Tuple of CurseAddons + :rtype: Tuple[base.CurseAddon] + """ + + # Search the plugins: + + return self.search(MinecraftWrapper.GAME_ID, MinecraftWrapper.BUKKIT, search) diff --git a/docs/source/basic/collection.rst b/docs/source/basic/collection.rst index ad70e59..04c9540 100644 --- a/docs/source/basic/collection.rst +++ b/docs/source/basic/collection.rst @@ -341,9 +341,23 @@ Check out this example of sorting by popularity: 'index' and 'pageSize' are used since search results are usually limited to 'pages' to save some bandwidth. +'pageSize' is the size of each page. +For example, if your page size is five, then you will get five results +with each search operation. -'index' is the page to retrieve, -and 'pageSize' is the size of each page. +'index' is NOT the page number. +Instead, it is the addon to start the search operation at. +For example, if you have index set at three, you will NOT be at page three. +instead, the search operation will start at the fourth addon. + +If you wish to traverse pages, you can use the 'set_page()' and 'bump_page()' methods. +The 'set_page()' method sets the index to the given page. +The 'bump_page()' method adds the pages to the index. + +For example, lets say you have a search parameter with an index of three, +and a page size of five. +If you set the page to three, then the index will be set to 15. +If you bump the page three times, then the index will be set to 18. Here is an example of getting the second page of search results: @@ -353,9 +367,9 @@ Here is an example of getting the second page of search results: search = client.get_search() - # Set the page index to 1: + # Set the index to page 2: - search.index = 1 + search.set_page(2) # Get the results: @@ -388,9 +402,9 @@ and print each name: print(addon.name) -'iter_search' only bumps the index after each call, -so you can start at a page by setting the 'index' -value on the SearchParam before passing it along. +'iter_search' calls 'bump_page()' after each call, +so you can start at a certain index +on the SearchParam before passing it along. The 'iter_search' does not alter any other parameters, so your search preferences will be saved. diff --git a/docs/source/basic/curse_inst.rst b/docs/source/basic/curse_inst.rst index 7dc549d..225e201 100644 --- a/docs/source/basic/curse_inst.rst +++ b/docs/source/basic/curse_inst.rst @@ -58,7 +58,7 @@ and how to decode it. Because cursepy is modular, this data can be literally anything! So having a standardized way to get this data is very important. -CIs also provide all addon info in a convent way. +CIs also provide all addon info in a convenient way. Most users do not want to manually parse request data! Finally, CI's convenience methods make using cursepy much easier. @@ -124,7 +124,7 @@ More info can be found in the API reference for CIs. assume that 'inst' is a valid CurseInstance of the type being described. -Before we gte into CI types, +Before we get into CI types, we will first go over common features every CI has. Every CI should have attributes which store the raw data and metadata of the request. @@ -303,7 +303,7 @@ Here is an example of a custom formatter that appends 'Super Slick!' to the end # Import BaseFormat: from cursepy.formatters import BaseFormat - + class SuperFormatter(BaseFormat): def format(self, data: str) -> str: @@ -369,7 +369,7 @@ and provides methods for getting sub and parent catagories. * icon - Icon of the category(CurseAttachment) * date - Date this category was created -If you read the into tutorial +If you read the intro tutorial (You did read the into tutorial right?), then you will know that catagories can have parent and sub-catagories. @@ -529,6 +529,12 @@ You can also compare the type of the dependency with the 'REQUIRED' and 'OPTIONA To get the addon and addon file this dependency is a member of, you can use the 'addon()' and 'file()' methods. +Sometimes, the CurseDependency will have limited information. +This can happen when you use the ForgeSVC handlers and get all addon files. +In this case, the CurseDependency will have limited information, +where the 'id' and 'file_id' will be None. +This is a limitation of the ForgeSVC backend, not cursepy. + Conclusion ========== diff --git a/docs/source/basic/intro.rst b/docs/source/basic/intro.rst index 001e3be..1cda9ed 100644 --- a/docs/source/basic/intro.rst +++ b/docs/source/basic/intro.rst @@ -102,13 +102,13 @@ cursepy Terms cursepy has a few concepts that might be better understood with some explanation. We only touch on these concepts, we will go into much greater detail later in this tutorial. Don't worry about completely understanding these for now, -just keep these concepts in mind. +just keep these concepts in mind. CurseInstance ------------- A 'CurseInstance' is a class that represents -CF information, and makes getting related info easier. +CF information, and makes getting related info easier. For example, there is a class called 'CurseAddon' which represents a game addon. The class diff --git a/docs/source/basic/wrap.rst b/docs/source/basic/wrap.rst index fbe7463..e8d92b7 100644 --- a/docs/source/basic/wrap.rst +++ b/docs/source/basic/wrap.rst @@ -40,6 +40,7 @@ We have the following constants: * MODPACKS - ID of the modpack category(4471) * MODS - ID of the mods category(6) * WORLDS - ID of the worlds category(17) +* BUKKIT - ID of the bukkit category(5) You can use these constants in the entry point methods. MinecraftWrapper also provides some methods to make searching easier. @@ -65,6 +66,11 @@ search_worlds Searches the worlds category for addons. +search_plugins +-------------- + +Searches the Bukkit plugins category for addons. + Conclusion ========== diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index a2b5e89..daa2810 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -2,10 +2,38 @@ Changelog ========= +1.3.0 +====== + +This update fixes some major bugs, +and corrects an issue with searching. + +Features Added +-------------- + +* Added the 'set_page()' and 'bump_page()' methods to the SearchParam class, which makes traversing pages easy +* Added the 'Bukkit Plugins' category to the MinecraftWrapper +* The MinecraftWrapper is now imported in the init file, so users can import the class like so: + +.. code-block:: python + + from cursepy import MinecraftWrapper + +(This will be the case for any new wrappers added) + +Bug Fixes +--------- + +* Fixed an issue in the code and docs where the index is treated as the page of results to retrieve, which is incorrect +* We now download addon files correctly +* We now load reduced dependency info when ForgeSVC handlers are used to retrieve all files for a particular addon +* Fixed the 'iter_search()' method to correctly stop iteration +* Fixed some random typos in the documentation + 1.2.0 ===== -Featured Added +Features Added -------------- * We now keep track of dependency info in the new CurseDependency class