.. _curse_inst: ======================= Curse Instance Tutorial ======================= Introduction ============ Welcome to the CurseInstance(CI) tutorial! This tutorial aims to give you an understanding of CIs, and how to use them efficiently! In the previous tutorial, you should have read about the HandlerCollection, and it's entry points. You should have noticed some talk about CurseInstances, which will be covered in great detail in this document. What is a CurseInstance? ======================== A 'CurseInstance' is a class that represents CurseForge(CF) information. For example, the CurseAddon class contains info such as the addon name, slug, id, release date, ect. You can access the name of the addon like so: .. code-block:: python # Print the name of the addon # (Assume 'addon' is a valid CurseAddon instance) print(addon.name) CIs also offer convince methods for interacting with said data. These convenience methods automatically make the necessary CurseClient calls with the correct arguments to get the relevant data. .. note:: You should have already read the :ref:`CurseClient tutorial `. If you haven't, you should really check it out, as CurseClient is a very important class! Why CurseInstances? ------------------- CurseInstances are in place to normalize CF data. They ensure that no matter what the cursepy configuration is, your program will always interact with the same classes. If this were not the case, then the end user using cursepy would have to determine what format the raw data being is in, 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 convenient way. Most users do not want to manually parse request data! Finally, CI's convenience methods make using cursepy much easier. As stated earlier, these methods take the steps to automatically get extra info. This makes the end user's life much easier, and allows for a more pythonic, class-based way of interacting with CF. CI's use the entry point methods of the CC that returned them. This means that they expect the handlers at the given location to accept the standard arguments, and return the standard objects. You shouldn't have to create CI's yourself, as the handlers you load should do this for you. The only time you should create CI's are if you are writing a handler! If you ever find yourself in a position where you have to create a CI, then you can pass arguments in the order they are documented, or have a look at the API reference. Creating CurseInstances ----------------------- CurseInstances inherit the python `dataclass object `_. This means that many components present in curse instances are implemented by the dataclass module, including the 'init' function and instantiation. This means that the init function is not explicitly defined, but is instead generated by the dataclass module. The order of parameters documented is the order that they must be provided to instantiate a dataclass. For example, if the parameters of a dataclass by the name of 'DummyInstance' is documented like this: * arg1 - First argument * arg2 - Second argument * arg3 - Third argument Then you will instantiate the class like so: .. code-block:: python inst = DummyInstance(1, 2, 3) Where '1' will be set to 'arg1', '2' will be set to 'arg2', and so on. ALL arguments specified are required. It is highly recommended that you look at the `dataclass module documentation `_, and you should check out the API reference for CurseInstances for more info. Types of CurseInstances ======================= We will now go through each CI, and lay out there parameters and methods. More info can be found in the API reference for CIs. .. note:: For all coming examples, assume that 'inst' is a valid CurseInstance of the type being described. 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. Raw data is the raw, unprocessed data from the protocol object. This is usually bytes that are encoded in some format. Raw data should really only be touched by people who need it! It is already decoded and put into a format that is accessible (The CI). Note that not all handlers attach this raw data, so be warned! Metadata is extra data provided by the protocol object. For example, the URLProtocol object provides status codes, request headers, reason strings, ect. about the HTTP connection. This data can be useful if you are interested in connection stats. Again, not all handlers attach this metadata, so be warned! Here is an example of printing both values: .. code-block:: # Print the raw data: print(inst.raw) # Print the meta data: print(inst.meta) Extra Functionality ------------------- Some CIs have extra functionality that allows them to do extra things, such as write content to an external file, or download data from a remote source to a custom file. .. _curse_write: Writer ______ A CI with writer functionality will allow you to write content to an external file. You can invoke this process by using the 'write' method: .. code-block:: python inst.write(PATH, append=False) Where 'PATH' is a path like object giving the pathname of the file to be written to. You can also specify if the content is to be appended to the external file by using the 'append' parameter. The CI determines what will be written to the external file. If a CI has this feature, then we will go over what exactly they write in this tutorial. .. _curse_download: Downloader __________ A CI with downloader functionality will allow you to download external content and write it to a file. You can invoke this process by using the 'download' method: .. code-block:: python inst.download(PATH) Where 'PATH', like in the writer, is a path like object giving the pathname of the file to be written to. Again, the CI determines what will be downloaded and written to the external file. If a CI has this feature, then we will go over exactly what they download and write. .. _curse_attach: CurseAttachment --------------- Represents an attachment on CF. * title - Title of the attachment * id - ID of the attachment * thumb_url - URL to the thumbnail of the attachment (If the attachment is an image, then the thumbnail is a smaller version of the image) * url - URL of the attachment * is_thumbnail - Boolean determining if this attachment is a thumbnail of an addon * addon_id - ID this addon is apart of * description - Description of this attachment CurseAttachments have the :ref:`download feature`, which means that you can download this attachment using the 'download' method: .. code-block:: python data = inst.download() This will download the raw bytes and return them. If you want to write this content to a file, then you can pass a path to the 'path' parameter, like so: .. code-block:: python written = inst.download('path/to/file.jpg') Where 'written' will be the number of bytes written. If you provide a directory instead of a file to write, then we will automatically use the default name as the file to write to. You can also download the thumbnail using the 'download_thumbnail' method, which operates in the same way. .. _curse_description: CurseDescription ---------------- Represents a description on CF. This description can be any HTMl text! * description - Raw HTML description text * formatter - Formatter attached to this description. The stored description is usually in HTML. This may make interpreting and displaying the description difficult. To alleviate this problem, CurseDescription allows for the registration of formatters that can change or alter the text. A formatter is a class that alters the description into something new. You can register valid formatters using the 'attach_formatter' method: .. code-block:: python inst.attach_formatter(FORM) Where 'FORM' is the formatter to attach. If the formatter is invalid, then we raise a TypeError exception. A formatter is valid if it inherits the 'BaseFormat' class. HandlerCollection objects can automatically attach formatters to CurseDescription objects if specified. You can pass a valid formatter to the 'default_formatter()' method on the HC, and the formatter will be attached to every CurseDescription object returned. Here is a list of all built in formatters: * NullFormatter - Does nothing! * StripHTML - Strips all HTML elements, leaving(In theory) valid text. * BSFormatter - Loads the HTML data into beautiful soup for further parsing. This formatter returns a bs4 instance, and beautiful soup MUST be installed, or an exception will be raised! Here is an example of setting a StripHTML as the default formatter for a HC: .. code-block:: python hc.default_formatter(StripHTML()) Where 'hc' is a valid HandlerCollection object. To get the formatted content, you can use the format method: .. code-block:: python cont = desc.format() Where 'desc' is a valid CurseDescription object. 'format' will send the description thorough the formatter, and return the content the formatter provides. You can also create your own custom formatters as well. Just inherit the 'BaseFormatter' class, and overload the 'format' method. The 'format' method should return the formatted content. Here is an example of a custom formatter that appends 'Super Slick!' to the end of the description: .. code-block:: python # Import BaseFormat: from cursepy.formatters import BaseFormat class SuperFormatter(BaseFormat): def format(self, data: str) -> str: """ Returns the description, but with 'Super Slick!' appended to the end. """ return data + 'Super Slick!' # Attach to a CurseDescription object: desc.attach_formatter(SuperFormatter()) CurseDescription objects can write content to an external file, as it has :ref:`writing functionality`. .. _curse_author: CurseAuthor ----------- Represents an author on CF. * id - ID of the author * name - Name of the author * url - URL to the authors page CurseAuthor classes is not necessary for CF development, and only acts as extra info if you want it. .. _curse_game: CurseGame --------- Represents a game on CF. * name - Name of the game * slug - Slug of the game * id - ID of the game * support_addons - Boolean determining if the game supports addons * icon_url - URL to the game icon * tile_url - URL to the image used for the game tile * cover_url - URL to the image used for the game cover * status - Used for determining the game status, defined by constants! * api_status - Determining if this game is public or private Each game has a status, which is defined by the (you guessed it) 'status' parameter. You can use these constants to identify the status: * [1]: DRAFT - This game is a draft, not meant to be used * [2]: TEST - Game is in testing, not meant to be used * [3]: PENDING_REVIEW - Game is pending review, not meant to be used * [4]: REJECTED - Game has been rejected from the backend, definitely not meant to be used * [5]: APPROVED - Game has been approved and is good to be used * [6]: LIVE - Game is live and (in theory) being used. This is the best game status! So, if you wanted to see if a given game is valid and live, then you can check the status: .. code-block:: python if game.status == CurseGame.LIVE: print('Game is live!') The 'api_status' parameter is used to determine if the game is public or private. You can use these constants to identify the status: * [1]: PRIVATE - Game is private and not available for use * [2]: PUBLIC - Game is public and available for use If you want to retrieve the objects that represent the catagories, you can use the 'categories' method to retrieve category info like so: .. code-block:: python cats = inst.catagories() This will return a tuple of :ref:`CurseCategory` objects representing each root category. CurseGame also makes searching addons a breeze. You can use the 'search' method to search for addons: .. code-block:: python addons = inst.search(SEARCH) Where SEARCH is a search param. This method will automatically fill in the necessary game ID for you, and will return a tuple of :ref:`CurseAddon` objects. CurseGame also as an 'iter_search()' method, which will traverse all pages of the search results. .. note:: If you need a primer on searching, check out the :ref:`CurseClient Tutorial `. .. _curse_category: CurseCategory ------------- Represents a CurseCategory, and provides methods for getting sub and parent catagories. * id - ID of the category * game_id - ID of the game the category is associated with * name - Name of the category * root_id - ID of this objects root category(None if there is no root ID) * parent_id - ID of this objects parent category(None if there is no root ID) * icon - Icon of the category(:ref:`CurseAttachment`) * url - URL to the category page * date - Date this category was created * slug - Slug of the addon If you read the :ref:`intro tutorial` (You did read the into tutorial right?), then you will know that catagories can have parent and sub-catagories. CurseCategory objects have methods for traveling though the hierarchy, and each returns CurseCategory objects representing these catagories. 'sub_categories()' returns a tuple of CurseCategory objects representing each sub-category, returns an empty tuple if there is no sub-categories. 'parent_category()' returns a CurseCategory object representing the parent category, returns None if there is no root category. 'root_category()' returns a CurseCategory object representing the root category, returns None if there is no root category. .. _curse_addon: CurseAddon ---------- Represents an addon on CurseForge. * name - Name of the addon * slug - Slug of the addon * summary - Summary of the addon(Not a full description) * url - URL of the addon page * lang - Language of the addon * date_created - Date this addon was created * date_modified - Date this addon was last modified * date_release - Date the addons latest release * id - ID of this addon * download_count - Number of times this addon has been downloaded * game_id - ID of the game this addon is in * available - Boolean determining if the addon is available * experimental - Boolean determining if the addon is experimental * authors - Tuple of :ref:`CurseAuthor` instances for this addon * attachments - Tuple of :ref:`CurseAttachments` associated with the object * category_id - ID of the category this addon is in * root_category - ID of the root category this addon is apart of * all_categories - Tuple of CurseCategory objects representing all the categories this addon is apart of * is_featured - Boolean determining if this addon is featured * popularity_score - Float representing this popularity score(Most likely used for ranking) * popularity_rank - int representing the addon game's popularity * allow_distribute - If this addon is allowed to be distributed * main_file_id - ID of the main file for this addon * status - Status of this addon, defined by constants! * wiki_url - URL to the addon wiki page * issues_url - URL to the addon issues page * source_url - URL to the addon source code To determine the status of the addon, you can use the constants: * [1]: NEW - This addon is new, no further progress has been made * [2]: CHANGED_REQUIRED - This addon needs to be changed in some way before it is approved * [3]: UNDER_SOFT_REVIEW - This addon is under soft review * [4]: APPROVED - This addon is approved and ready for use * [5]: REJECTED - This addon has been rejected from the backend, definitely not meant to be used * [6]: CHANGES_MADE - This addon has been changed since it's last review * [7]: INACTIVE - This addon is inactive and not being maintained * [8]: ABANDONED - This addon is abandoned and not being maintained * [9]: DELETED - This addon has been deleted * [10]: UNDER_REVIEW - This addon is under review CurseAddon objects do not keep the description info! A special call must be made to retrieve this. CurseAddon offers a property that that can retrieve the description as a CurseDescription object: .. code-block:: python desc = inst.description You can get the files associated with this addon by using the 'file' method: .. code-block:: python file = inst.file(ID) Where ID is the ID of the file to retrieve. This method returns a :ref:`CurseFile` object representing the files (We will go over :ref:`CurseFile` objects later in this tutorial!). If you want a list of all files associated with the addon, you can use the 'files()' method, which returns a tuple of :ref:`CurseFile` objects. You can retrieve the :ref:`CurseGame` object representing the game this addon is apart of using the 'game()' method. You can also get a :ref:`CurseCategory` object representing the category this addon is apart of by using the 'category()' method: .. code-block:: python # Get the game: game = inst.game() # Get the category: cat = inst.category() .. _curse_file: CurseFile --------- Represents a file on CF. * id - ID of the file * addon_id - ID of the addon this file is apart of * display_name - Display name of the file * file_name - File name of the file * date - Date the file was uploaded * download_url - Download URL of the file * length - Length in bytes of the file * version - Version of the game needed to work this file * dependencies - Tuple of :ref:`CurseDependency` objects for this file * game_id - ID of the game this file is apart of * is_available - Boolean determining if the file is available * release_type - Release type of the file, defined by constants! * file_status - Status of the file, also defined by constants! * hashes - Tuple of :ref:`CurseHash` objects representing file hashes * download_count - Number of times this file has been downloaded To determine the release type of the file, you can use the constants: * [1]: RELEASE - This file is a release * [2]: BETA - This is a beta file * [3]: ALPHA - This is an alpha file To determine the file status, you can use the constants: * [1]: PROCESSING - This file is being processed and checked * [2]: CHANGES_REQUIRED - This file needs to be changed in some way before it is approved * [3]: UNDER_REVIEW - This file is under review * [4]: APPROVED - This file is approved and ready for use * [5]: REJECTED - This file has been rejected from the backend, definitely not meant to be used * [6]: MALWARE_DETECTED - This file has been detected as malware, you *really* should not use it! * [7]: DELETED - This file has been deleted * [8]: ARCHIVED - This file has been archived * [9]: TESTING - This file is being tested * [10]: RELEASED - This file is released and ready to be used * [11]: READY_FOR_REVIEW - This file is ready to be reviewed * [12]: DEPRECATED - This file has been marked as deprecated * [13]: BAKING - This file is being baked (?) * [14]: AWAITING_PUBLISHING - This file is awaiting publishing * [15]: FAILED_PUBLISHING - This file failed to publish To determine if a file is good to be used, you can use the 'good_file()' method: .. code-block:: python if inst.good_file(): print "This file is good to use!" else: print "This file is not good to use!" This method simply checks if the file is available, if the file is released, and if the file status is RELEASED. Just because a file is not good does not mean it can't be used! On top of this, just because a file is good does not mean it will work properly. Our only understanding of the file is what the backend says it is. Production ready files could be poorly made, and non-production ready experimental files could also be valid. To get the changelog of the file, you can use the 'changelog' property: .. code-block:: python desc = inst.changelog This will return a :ref:`CurseDescription` object representing the description. As stated earlier, CI's use the entry point methods of the HC that returned them. This means that the :ref:`CurseDescription` object will have the default formatter attached to it. If you want all dependencies for a file, then you can find them under the 'dependencies' parameter. You can also get certain dependencies by using the 'get_dependencies()' method: .. code-block:: python deps = inst.get_dependencies(DEPEN_TYPE) Where DEPEN_TYPE is a dependency type, defined by the :ref:`CurseDependency` constants. This method will only return dependencies of the specified type. You can also use the 'get_addon()' method to retrieve a :ref:`CurseAddon` object representing the addon this file is attached to. The CurseFile class also has :ref:`download functionality`. You can use the 'download()' method to download this file. .. note:: This next section is only relevant to the official CurseForge API! If you aren't using the CurseForge backend, then this information (probably) won't apply to you. In the official CurseForge API, addons can specify if they are approved for distribution over 3rd party services. Any projects created before the release of the official CurseForge API (around 2020-2021) will have this enabled by default. All new projects created afterwards will have this disabled by default. If 3rd party distribution is enabled, then the CurseForge API will serve a download URL for each addon file. Otherwise, the CurseForge API will simply server 'None', and the 'download()' method will fail. You can get around this by using the 'guess_download()' method: .. code-block:: python url = inst.guess_download() This method uses some attributes to create a best guess download URL that may work (more testing is required!). This URL will download the addon files from the official CurseForge Content Distribution Network, regardless of the handler that retrieved the data. It is highly recommended to use a download URL provided by the backend if possible! Once you have the URL, you can set the 'download_url' parameter and then download the file: .. code-block:: python inst.download_url = inst.guess_download() inst.download() .. _curse_dependency: CurseDependency --------------- Represents a file dependency on CF. * id - ID of the dependency * addon_id - ID of the addon this dependency is apart of * file_id - ID of the file this dependency is apart of * type - Type of the dependency, defined using constants! * required - Boolean determining if the dependency is required To determine the addon type, you can use the constants: * [1]: EMBEDDED_LIBRARY - This dependency is an embedded library * [2]: OPTIONAL - This dependency is optional * [3]: REQUIRED - This dependency is required * [4]: TOOL - This dependency is an optional tool * [5]: INCOMPATIBLE - This dependency is incompatible * [6]: INCLUDE - This dependency is an include file To determine if the dependency is required, you can use the 'required' parameter. This parameter under the hood is a property which returns 'True' if the dependency is required. and 'False' if it is not. To get the :ref:`CurseAddon` and :ref:`CurseFile` objects this dependency is a member of, you can use the 'addon()' and 'file()' methods respectively. .. _curse_hash: CurseHash --------- Represents a file hash on CF. * hash - Hash of the file * algorithm - Algorithm used to generate the hash, defined using constants! You can determine the algorithm by using these constants: * [1]: SHA1 - SHA1 algorithm * [2]: MD5 - MD5 algorithm These objects can be used to check the integrity of files, and determine if they are valid. Some backends may not provide hashes for certain files, or at all! Conclusion ========== That concludes this tutorial on CurseInstance objects! If you have any other questions about the intricacies of CurseInstances, then you should check out the API Reference!