diff --git a/.github/actions/build-project/action.yml b/.github/actions/build-project/action.yml new file mode 100644 index 0000000..69aee02 --- /dev/null +++ b/.github/actions/build-project/action.yml @@ -0,0 +1,21 @@ +name: Build the project +description: Installs project dependencies and builds the project + +inputs: + latest_python: + default: false + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ">=3.5" + check-latest: ${{ inputs.latest_python }} + + - name: Build documentation + shell: bash + run: | + python3 -m pip install .[docs] + make html diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 0000000..b479959 --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,28 @@ +name: Publish GH Pages + +on: + push: + branches: + - master + +permissions: + contents: write + pages: write + id-token: write + +jobs: + build-docs: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build the project + uses: ./.github/actions/build-project + + - name: Deploy to Github Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build/html diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..78540d9 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,44 @@ +name: PR Checks + +on: + pull_request: + branches: + - "**" + +jobs: + build-linux: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build the project + uses: ./.github/actions/build-project + with: + latest_python: true # always use the latest python in PR checks + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-${{ github.run_id }}-${{ github.run_number }} + path: ./build + retention-days: 14 + + - name: Lint the project + shell: bash + run: | + python -m pip install .[dev] + make lint + + + build-windows: + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build the project + uses: ./.github/actions/build-project + with: + latest_python: true # always use the latest python in PR checks diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..57fe352 --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ + +.DEFAULT_GOAL = all + +PROJECT = rule34py +VERSION = $(shell git describe --tags) + +PYTHON3 ?= python3 +PYTHON_BUILD = $(PYTHON3) -m build +RUFF = $(PYTHON3) -m ruff +SPHINX_BUILD = $(PYTHON3) -m sphinx +PYTEST = $(PYTHON3) -m pytest + +builddir ?= build +srcdir = rule34Py + +# Installation Directories +prefix ?= /usr/local + +docdir ?= $(prefix)/share/doc/$(project) +htmldir ?= $(docdir)/html + + +# REAL TARGETS # +################ + + +# PHONY TARGETS # +################# + +all : html + $(PYTHON3) -m build --wheel --outdir $(builddir) --verbose +.PHONY : all + + +check : + PYTHONPATH=$(srcdir)/.. $(PYTEST) tests/unit/ +.PHONY : check + + +clean : mostlyclean + find ./ -depth -path '**/.pytest_cache*' -print -delete + find ./ -depth -path '**/__pycache__*' -print -delete + $(RUFF) clean +.PHONY : clean + + +dist : + $(PYTHON_BUILD) --sdist --outdir $(builddir) +.PHONY : dist + + +distclean : clean +.PHONY : distclean + + +html : + $(SPHINX_BUILD) --builder html docs $(builddir)/html +.PHONY : html + + +lint : + $(RUFF) check $(srcdir) +.PHONY : lint + + +mostlyclean : + rm -rf $(builddir) + find ./ -depth -path '**/rule34Py.egg-info*' -print -delete +.PHONY : mostlyclean diff --git a/NOTICE.md b/NOTICE.md index 6fbf269..50fca89 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,6 +1,8 @@ +# NOTICE + The following third-party software is used in this project: -# Responses +## Responses * Licensed under the Apache License, Version 2.0 * Copyright (c) David Cramer diff --git a/README.md b/README.md index 2fd2841..0d41f1b 100644 --- a/README.md +++ b/README.md @@ -4,31 +4,25 @@ ![GPL-3.0](https://img.shields.io/github/license/b3yc0d3/rule34Py) [![](https://img.shields.io/pypi/v/rule34Py)](https://pypi.org/project/rule34Py/) [![](https://img.shields.io/pypi/dm/rule34py?color=blue)](https://pypi.org/project/rule34Py/) -Python api wrapper for [rule34.xxx](https://rule34.xxx/). +Python API wrapper for [rule34.xxx](https://rule34.xxx/). -## Getting Started -#### Install it using pip -``` -pip install rule34py -``` +## Installation -#### Building it from Source -``` -git clone https://github.com/b3yc0d3/rule34Py.git -cd rule34Py -python3 -m build +[rule34Py](https://pypi.org/project/rule34Py/) is available directly from the Python Package Index and can be installed via `pip`. + +```bash +pip install rule34Py ``` -## Documentation -You can find the documentation [here](https://github.com/b3yc0d3/rule34Py/tree/master/docs). +Or you can build it from source using this project. +See the [Developer Guide](https://b3yc0d3.github.io/rule34Py/dev/developer-guide.html) for more information. + -> [!NOTE] -> The documentation might move in the future. +## Quickstart -## Code Snippet -```py +```python from rule34Py import rule34Py r34Py = rule34Py() @@ -54,62 +48,9 @@ random = r34Py.random_post() random_id = r34Py.random_post_id() ``` -## Development -Follow these steps to setup everything needed to develop on rule34Py. - -Currently this setup guide only shows how it is done on unix-like systems. - -### Clone This Repository -``` -git clone https://github.com/b3yc0d3/rule34Py.git - -cd rule34Py - -git checkout develop -``` - -### Setting Up Virtual Python Environment -``` -python -m venv venv - -source venv/bin/activate -``` - -To deactivate the virtual environment type the following in your terminal -``` -deactivate -``` - -### Install and Build rule34Py in the Virtual Environment -``` -python3 -m build - -pip install -e . -``` - -### Running the Test Suite - -This project is tested by an organic `pytest` suite, stored under the `:tests/` directory. - -See the [`tests/README.md`](./tests/README.md) file for instructions on how to run the test suite. - - -### Committing your Changes -- Branch name should be prefixed with - - `fix-` when fixing an bug/error - - `feat-` when a feature got added - - `chore-` everything else that doesn't fall in the above categories -- The title must be descriptive, what your pull request changes/does. -- Write a breve description of what the pull request does/solves in the commit. -- If your pull request fixes an issue, please mention that issue in the commit title. - -Example structure of a commit message -``` -here goes the title of the commit +## Documentation -Here goes the description -``` +This project has extensive [documentation](https://b3yc0d3.github.io/rule34Py/), hosted on the upstream Github Pages. It includes additional **Tutorials**, **User Guides**, **API Documentation**, and more. -The title shall not be longer then 50 characters. -**Select the `develop` branch for pull requests.** +See the [Contributing Guide](https://b3yc0d3.github.io/rule34Py/dev/contributing.html) for information about how to contribute to this project and file bugs. diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index af4ed45..0000000 --- a/docs/README.md +++ /dev/null @@ -1,566 +0,0 @@ -# Rule34Py Documentation - -Last edited at `31-12-2024` by [ripariancommit](https://github.com/ripariancommit) and [b3yc0d3](https://github.com/b3yc0d3) - -Parameters that are prefixed by an `?` are optional. - -## rule34Py -rule34.xxx API wrapper. - -### Functions -
-__init__() - rule34.xxx API wrapper - -rule34.xxx API wrapper. - -
- -
- -get_comments(post_id: int) -> list[PostComment] - Retrieve comments of post by its id - -Retrieve comments of post by its id. - -##### Parameters -| Parameter | Type | Description | -|:----------|:------|:------------| -| `post_id` | `int` | Posts id. | - -##### Returns -**`list[PostComment]`** - List of [PostComment](#postcomment)s. -
- -
-get_pool(pool_id: int, ?fast: bool) -> list[Post|int] - Retrieve pool by its id - -Retrieve pool by its id. - -**Be aware that if "fast" is set to False, it may takes longer.** - -##### Parameters -| Parameter | Type | Description | -|:----------|:-------|:-----------------------------------------------------------------------------------------| -| `pool_id` | `int` | Pools id. | -| `fast` | `bool` | Fast "mode", if set to true only a list of post ids will be returned. (default *false*). | - -##### Returns -**`list[Post|int]`** - List of [post](#post) objects or post ids if `fast` is set to true. -
- -
-get_post(post_id: int) -> Post - Get post by its id - -Get post by its id. - -##### Parameters -| Parameter | Type | Description | -|:----------|:------|:------------| -| `post_id` | `int` | Id of post. | - -##### Returns -**`Post`** - [Post](#post) object. -
- -
-icame() -> list[ICame] - Retrieve list of top 100 iCame list - -Retrieve list of top 100 iCame list. - -##### Returns -**`list[ICame]`** - List of [iCame](#icame) objects. -
- -
-random_post(?tags: list) -> Post - Get a random post - -Get a random post. - -##### Parameters -| Parameter | Type | Description | -|:----------|:------------|:----------------------------------------------------------------------| -| `tags` | `list[str]` | Tag list to search. If none, post will be used regardless of it tags. | - -##### Returns -**`Post`** - [Post](#post) object. -
- -
-search(self, tags: list[str], ?page_id: int, ?limit: int, ?deleted: bool) -> list[Post] - Search for posts - -Search for posts - -##### Parameters -| Parameter | Type | Description | -|:-------------------|:------------|:---------------------------------------------------------------| -| `tags` | `list[str]` | List of tags. | -| `page_id` | `int` | Page number/id. | -| `limit` | `init` | Limit for posts returned per page (max. 1000). | - -##### Returns -**`list[Post]`** - List of [Post](#post) objects for matching posts. -
- -
-iter_search(self, tags: list[str], max_results: (int | None)) -> Iterator[Post] - Search for posts - -Search for posts, iterate through results, one element at a time. - -##### Parameters -| Parameter | Type | Description | -|:-------------------|:------------ |:---------------------------------------------------------------| -| `tags` | `list[str]` | List of tags. | -| `max_results` | `int | None` | The maximum number of results to return before ending the iteration.
If 'None', then iteration will continue until the end of the results. Defaults to 'None'. | - -##### Returns -**`Iterator[Post]`** - Iterator of [Post](#post) objects for matching posts. -
- -
-tag_map() -> dict[str, str] - Retrieve the tag map points - -Retrieve the tag map points. - -##### Returns -**`dict[str, str]`** - A mapping of country and district codes to their top tag. 3-letter keys are ISO-3 character country codes, 2-letter keys are US-state codes. -
- -
-tagmap() -> list[TopTag] - Retrieve list of top 100 global tags - -Retrieve list of top 100 global tags. - -**This method is deprecated in favor of the top_tags() method.** - -##### Returns -**`list[TopTag]`** - List of global top 100 tags. See [TopTag](#toptag). -
- -
-top_tags() -> list[TopTag] - Retrieve list of top 100 global tags - -Retrieve list of top 100 global tags. - -##### Returns -**`list[TopTag]`** - List of top 100 tags, globally. See [TopTag](#toptag). -
- -
-version -> str - Rule34Py version - -Rule34Py version. - -##### Returns -**`str`** - Version of rule34py. -
- -## Post - -### Functions - -
-__init__(id: int, hash: str, score: int, size: list[int, int], image: str, preview: str, sample: str, owner: str, tags: list[str], file_type: str, directory: int, change: int) - Post Class - -Post Class - -##### Parameters -| Parameter | Type | -|:------------|:-----------------| -| `id` | `int` | -| `hash` | `str` | -| `score` | `int` | -| `size` | `list[int, int]` | -| `image` | `str` | -| `preview` | `str` | -| `sample` | `str` | -| `owner` | `str` | -| `tags` | `list[str]` | -| `file_type` | `str` | -| `directory` | `int` | -| `change` | `int` | - -
- -
-from_json(json: str) -> Post - Create Post from json data - -Create Post from json data. - -##### Parameters -| Parameter | Type | Description | -|:----------|:------|:------------------------------------| -| `json` | `str` | Json data from rule34.xxx REST Api. | - -##### Returns -**`Post`** - Post object. -
- -### Properties -
-change -> int - Post last update time - -Post last update time. - -Retrieve the timestamp indicating the last update/change of the post, as unix time epoch. - -##### Returns -**`int`** - UNIX Timestamp representing the post's last update/change. -
- -
-content_type -> str - Get type of post data (e.g. gif, image etc.) - -Get type of post data (e.g. gif, image etc.). - -Represents the value of `file_type` from the api. - -##### Returns -**`str`** - A string indicating the type of the post. -**Possible values** -- `image` Post is of an image. -- `gif` Post is of an animation (gif, webm, or other format). -- `video` Post is of a video. -
- -
-directory -> int - Get directory id of post - -Get directory id of post. - -##### Returns -**`int`** - Unknown Data. -
- -
-hash -> str - Obtain the unique hash of post - -Obtain the unique hash of post. - -##### Returns -**`str`** - The hash associated with the post. -
- -
-id -> int - Obtain the unique identifier of post - -Obtain the unique identifier of post. - -##### Returns -**`int`** - The unique identifier associated with the post. -
- -
-image -> str - Get the image of the post - -Get the image of the post. - -##### Returns -**`str`** - Image url for the post. -
- -
-owner -> str - Get username of post creator - -Get username of post creator. - -##### Returns -**`str`** - Username of post creator. -
- -
-rating -> str - Retrieve the content rating of the post - -Retrieve the content rating of the post. - -##### Returns -**`str`** - A string representing the post's rating. -**Possible Values:** -- `e` Explicit -- `s` Safe -- `q` Questionable -
- -
-sample -> str - Get the sample image/video of the post - -Get the sample image/video of the post. - -##### Returns -**`str`** - Sample data url for the post. -
- -
-score -> int - Get the score of post - -Get the score of post. - -##### Returns -**`int`** - The post's score. -
- -
-size -> list[int, int] - Retrieve actual size of post's image - -Retrieve actual size of post's image. - -##### Returns -**`list[int, int]`** - List of [width, height] representing the image dimensions. -
- -
-tags -> list[str] - Get tags of post - -Get tags of post. - -##### Returns -**`list[str]`** - List of posts tags. -
- -
-thumbnail -> str - Get the thumbnail image of the post - -Get the thumbnail image of the post. - -##### Returns -**`str`** - Thumbnail url for the post. -
- -
-video -> str - Get the video of the post - -Get the video of the post. - -##### Returns -**`str`** - Video url for the post. -
- -## PostComment -PostComment - -Class to represent a comment under a post. - -### Functions - -
-__init__(id: int, owner_id: int, body: str, post_id: int, creation: str) - PostComment Class - -PostComment Class - -##### Parameters -| Parameter | Type | -|:-----------|:------| -| `id` | `int` | -| `owner_id` | `int` | -| `body` | `str` | -| `post_id` | `int` | -| `creation` | `int` | -
- -### Properties - -
-author_id -> int - Id of the comments author - -Id of the comments author. - -##### Returns -**`int`** - Id of comment author. -
- -
-body -> str - Content of the comment - -Content of the comment. - -##### Returns -**`str`** - Content of the comment. -
- -
-creation -> int - Timestamp when the comment was created [ATTENTION] - -Timestamp when the comment was created. - -**Important: currently rule34.xxx api returns the time *when your -api request was made* and _not_ the time when the comment was created.** - -##### Returns -**`int`** - Timestamp when comment was created. -
- -
-id -> int - Comments unique id - -Comments unique id. - -##### Returns -**`int`** - Comments unique id. -
- -
-post_id -> int - Id of post, to whom the comment belongs - -Id of post, to whom the comment belongs. - -##### Returns -**`int`** - Id of parent post. -
- -## Stat -Stat Class. - -Generic Stat class, mostly used to Top nth lists. - -### Functions - -
-__init__(place: int, amount: int, username: str) - Stat class - -Stat class. - -##### Parameters -| Parameter | Type | -|:-----------|:------| -| `place` | `int` | -| `amount` | `int` | -| `username` | `str` | -
- -### Properties - -
-amount -> int - Get amount/count of it - -Get amount/count of it. - -##### Returns -**`int`** - Amount of something related to this stat. -
- -
-place -> int - Get index/positional place of the stat - -Get index/positional place of the stat. - -##### Returns -**`int`** - Positional index. -
- -
-username -> str - Get username or name of character related to this stat - -Get username or name of character related to this stat. - -##### Returns -**`str`** - Related username / name of a character to this stat. -
- -## TopTag -TopTag Class. - -### Functions - -
-__init__(rank: int, tagname: str, percentage: int) - TopTag Class - -TopTag Class. - -##### Parameters -| Parameter | Type | -|:-------------|:------| -| `rank` | `int` | -| `tagname` | `str` | -| `percentage` | `int` | -
- -
-__from_dict(json: dict) -> TopTag - Create TopTag class from JSON data - -Create TopTag class from JSON data. - -##### Parameters -| Parameter | Type | Description | -|:----------|:-------|:------------------------------------| -| `json` | `dict` | JSON data from rule34.xxx REST Api. | - -##### Returns -**`TopTag`** - TopTag object. -
- -### Properties - -
-percentage -> float - Get tags percentage in use - -Get tags percentage in use. - -##### Returns -**`int`** - Tags usage as percentage value. -
- -
-rank -> int - Get tags rank - -Get tags rank. - -##### Returns -**`int`** - Get rank of the tag. -
- -
-tagname -> str - Get tags name - -Get tags name. - -##### Returns -**`str`** - Get name of the tag. -
- -## ICame -ICame Class. - -ICame chart item. - -### Functions - -
-__init__(character_name: str, count: int) - ICame Class - -ICame Class. - -iCame chart item. - -##### Parameters -| Parameter | Type | -|:-----------------|:------| -| `character_name` | `str` | -| `count` | `int` | -
- -### Properties - -
-character_name -> str - Get name of character - -Get name of character. - -##### Returns -**`str`** - Name of character. -
- -
-count -> int - Get count of how often people came on the character - -Get count of how often people came on the character. - -##### Returns -**`int`** - Cum count. -
- -
-tag_url -> str - Get url of tag - -Get url of tag. - -##### Returns -**`str`** - Url of tag. -
diff --git a/docs/_static/cf_clearance_example.webp b/docs/_static/cf_clearance_example.webp new file mode 100644 index 0000000..bd403d4 Binary files /dev/null and b/docs/_static/cf_clearance_example.webp differ diff --git a/docs/_static/user-agent_example.webp b/docs/_static/user-agent_example.webp new file mode 100644 index 0000000..3dbc5a4 Binary files /dev/null and b/docs/_static/user-agent_example.webp differ diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 0000000..48ed2a2 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,8 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 2 + :caption: Modules: + + rule34Py/__init__ diff --git a/docs/api/rule34Py/__init__.rst b/docs/api/rule34Py/__init__.rst new file mode 100644 index 0000000..2419a8a --- /dev/null +++ b/docs/api/rule34Py/__init__.rst @@ -0,0 +1,22 @@ +rule34Py +======== + + +.. automodule:: rule34Py + :members: + :private-members: + :undoc-members: + + +.. toctree:: + :maxdepth: 1 + :caption: Submodules + + api_urls + html + icame + pool + post + post_comment + rule34 + toptag diff --git a/docs/api/rule34Py/api_urls.rst b/docs/api/rule34Py/api_urls.rst new file mode 100644 index 0000000..3a13cda --- /dev/null +++ b/docs/api/rule34Py/api_urls.rst @@ -0,0 +1,5 @@ +rule34Py.api\_urls +================== + +.. automodule:: rule34Py.api_urls + :members: diff --git a/docs/api/rule34Py/html.rst b/docs/api/rule34Py/html.rst new file mode 100644 index 0000000..a30c777 --- /dev/null +++ b/docs/api/rule34Py/html.rst @@ -0,0 +1,5 @@ +rule34Py.html +============= + +.. automodule:: rule34Py.html + :members: diff --git a/docs/api/rule34Py/icame.rst b/docs/api/rule34Py/icame.rst new file mode 100644 index 0000000..c920164 --- /dev/null +++ b/docs/api/rule34Py/icame.rst @@ -0,0 +1,5 @@ +rule34Py.icame +============== + +.. automodule:: rule34Py.icame + :members: diff --git a/docs/api/rule34Py/pool.rst b/docs/api/rule34Py/pool.rst new file mode 100644 index 0000000..c59c2c7 --- /dev/null +++ b/docs/api/rule34Py/pool.rst @@ -0,0 +1,5 @@ +rule34Py.pool +============= + +.. automodule:: rule34Py.pool + :members: diff --git a/docs/api/rule34Py/post.rst b/docs/api/rule34Py/post.rst new file mode 100644 index 0000000..872494e --- /dev/null +++ b/docs/api/rule34Py/post.rst @@ -0,0 +1,5 @@ +rule34Py.post +============= + +.. automodule:: rule34Py.post + :members: diff --git a/docs/api/rule34Py/post_comment.rst b/docs/api/rule34Py/post_comment.rst new file mode 100644 index 0000000..a762413 --- /dev/null +++ b/docs/api/rule34Py/post_comment.rst @@ -0,0 +1,5 @@ +rule34Py.post\_comment +====================== + +.. automodule:: rule34Py.post_comment + :members: diff --git a/docs/api/rule34Py/rule34.rst b/docs/api/rule34Py/rule34.rst new file mode 100644 index 0000000..0e7602a --- /dev/null +++ b/docs/api/rule34Py/rule34.rst @@ -0,0 +1,7 @@ +rule34Py.rule34 +=============== + +.. automodule:: rule34Py.rule34 + :members: + :undoc-members: + :private-members: _get diff --git a/docs/api/rule34Py/toptag.rst b/docs/api/rule34Py/toptag.rst new file mode 100644 index 0000000..5d4ac38 --- /dev/null +++ b/docs/api/rule34Py/toptag.rst @@ -0,0 +1,5 @@ +rule34Py.toptag +=============== + +.. automodule:: rule34Py.toptag + :members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..52ff878 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,57 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +from datetime import datetime +from pathlib import Path +import tomllib as toml + +PROJECT_ROOT = Path(__file__).parent.resolve() / ".." + +# Read the pyproject.toml +with open(PROJECT_ROOT / "pyproject.toml", "rb") as fp_pyproject: + pyproject = toml.load(fp_pyproject) +authors = [m["name"] for m in pyproject["project"]["authors"]] + +project = pyproject["project"]["name"] +copyright = str(datetime.now().year) + ', b3yc0d3' +author = ", ".join(authors) +release = pyproject["project"]["version"] + + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx_mdinclude", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.duration", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", +] + +templates_path = ['_templates'] +exclude_patterns = [] + +# sphinx.ext.autodoc configuration # +autodoc_default_options = { +} +autodoc_typehints = "both" # Show typehints in the signature and as content of the function or method + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "requests": ("https://requests.readthedocs.io/en/latest/", None), +} + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] diff --git a/docs/dev/contributing.rst b/docs/dev/contributing.rst new file mode 100644 index 0000000..9f81802 --- /dev/null +++ b/docs/dev/contributing.rst @@ -0,0 +1,39 @@ +============ +Contributing +============ + +Thanks for taking an interest in contributing to the rule34Py project! + +* This project's **canonical upstream** is at https://github.com/b3yc0d3/rule34Py. +* File **bugs**, **enhancement requests**, and other **issues** to the GH issue tracker at https://github.com/b3yc0d3/rule34Py/issues. +* See the `Developer Guide <./developer-guide.html>`_ for information about how to **build** this project from source and run tests. + + +Submitting Changes +================== + +#. Base your development branch off of the `upstream `_ ``develop`` reference. + +#. Before committing your changes, run the **project linter** using ``make``. It will use the ``ruff`` to lint all the project sources. + + .. code-block:: bash + + pip install .[dev] + make lint + + Fix or respond to any findings in the linter. + +#. Run the project's test suite against your changes. Ensure that all tests pass. + + .. code-block:: bash + + pip install .[test] + make check + +#. Write a good commit message. If you are unsure of how, `this cbeams article `_ gives reasonable suggestions. Commit your changes. + +#. Fork the canonical upstream repository on github. [`GitHub Docs `_] + +#. Push your development branch to your own fork. `Open a new Pull Request `_ against the upstream `develop` ref. + +#. Submit your PR. Respond to any PR build failures or feedback from the maintainers. diff --git a/docs/dev/developer-guide.rst b/docs/dev/developer-guide.rst new file mode 100644 index 0000000..d12d522 --- /dev/null +++ b/docs/dev/developer-guide.rst @@ -0,0 +1,120 @@ +=============== +Developer Guide +=============== + + +tl;dr +===== + +.. code-block:: bash + + pip install . .[docs] .[dev] .[test] + make all # build wheel + make check # run unit tests + make dist # build sdist + make html # build documentation + make lint # run project linter + + +Project Layout +============== + +* ``.github/`` contains GitHub specific workflows, actions, and templates. + +* ``docs/`` contains project documentation, including the source for the sphinx-generated documentation. + +* ``rule34Py/`` contains the source code for the ``rule34Py`` package. + +* ``tests/`` contains the project test suite. + + * ``tests/unit/`` contains the project unit-tests, which can be run without building or installing the project. + + +Building the project from source +================================ + +This project can be built directly from source using a few host tools. +Supported development environments include: + +- Ubuntu 24.04 +- Windows + +The following instructions are written on the assumption that you are building in a Linux environment. + +#. Start by cloning the repository with ``git``. + + .. code-block:: bash + + git clone https://github.com/b3yc0d3/rule34Py.git + cd rule34Py + +#. (Optional. Recommended.) Setup a python virtual environment. + + .. code-block:: bash + + python -m venv .venv + echo "/.venv" >>.git/info/exclude # excludes your venv from git tracking + source .venv/bin/activate + +#. Install project python dependencies from the ``pyproject.toml`` file. Use ``GNU Make`` to build the project. The ``all`` make target (the default) builds the project's wheels. + + .. code-block:: bash + + pip install . .[dev] + make all + + Build output will be placed in the ``:build/`` directory in your workspace. + +Other ``make`` targets are supported to ``clean`` the project and build other artifacts. +Generally, the project ``Makefile`` honors the `GNU Standard Targets `_ specification. + + +Running the project test suite +============================== + +#. Setup your build environment as in the "Building the project from source" section. + +#. Install the optional test dependencies and invoke their ``make`` target. + + .. code-block:: bash + + pip install .[test] + make test + +For more information, reference the ``:tests/README.md`` file. + + +Building the project documentation +================================== + +#. Setup your build environment as in the "Building the project from source" section. + +#. Install the optional docs dependencies and invoke their ``make`` target. + + .. code-block:: bash + + pip install .[docs] + make html + + Build output will be placed in the ``:build/html/`` directory. + +#. (Optional.) Host the build output locally to test changes. + + .. code-block:: bash + + cd build/html + python -m http.server 8080 + + Python will host the docs site at http://localhost:8080. + + +Integrating this project +======================== + +This project is `licensed <./license.html#license>`_ under the GPLv3 license. +Ensure that your project's licensing strategy is compatible with the GPL. +For more information, reference the GNU reference guide for GPLv3 `here `_. + +All direct dependencies of this project are either GPL licensed, or are licensed more permissively. +But testing code does call the ``reponses`` module, which is licensed under the Apache 2.0 license. +Reference the `:NOTICE.md <./license.html#notice>`_ file for more information. diff --git a/docs/dev/index.rst b/docs/dev/index.rst new file mode 100644 index 0000000..cc38018 --- /dev/null +++ b/docs/dev/index.rst @@ -0,0 +1,12 @@ + +Developer Documentation +======================= + +This section contains documentation primarily useful to developers who would like to integrate or contribute-to this project. + +.. toctree:: + :maxdepth: 1 + + developer-guide + contributing + license diff --git a/docs/dev/license.rst b/docs/dev/license.rst new file mode 100644 index 0000000..8911537 --- /dev/null +++ b/docs/dev/license.rst @@ -0,0 +1,9 @@ + +LICENSE +======= + +.. literalinclude:: ../../LICENSE + + +.. NOTICE.md already includes an H1 header. +.. mdinclude:: ../../NOTICE.md diff --git a/docs/guides/captcha-clearance.rst b/docs/guides/captcha-clearance.rst new file mode 100644 index 0000000..583b6df --- /dev/null +++ b/docs/guides/captcha-clearance.rst @@ -0,0 +1,69 @@ +==================================== +How to clear Rule34 captcha defenses +==================================== + +The `rule34.xxx `_ interactive (PHP) site occasionally enables Cloudflare captcha defenses, to limit non-human access to the interactive site. +As of writing (2025-05-17), captcha defenses are enabled. + +The `api.rule34.xxx `_ REST API endpoint does not have captcha defenses. +Workflows in the rule34Py module prefer to use the REST API endpoint wherever possible. +But not all features of the interactive site are supported by the REST endpoint, and so the module defaults to parsing the interactive site's HTML. + +In order to enable these interactive workflows, while providing at least some respect to the site's captcha defense, rule34Py allows the user to: complete the captcha defense manually in their browser, and then give their captcha clearance to the python module to use in subsequent interactions. + +Note that the module will rate-limit requests to the interactive site to 1 request per second, as a further courtesy to the site owners. +We also recommend avoiding these workflows entirely in your application design, out of respect. + +Reference the `rule34Py.rule34Py <../../api/rule34Py/rule34.html#rule34Py.rule34.rule34Py>`_ class documentation for an indication of which workflows have these limits. + + +Clearing the defenses +===================== + +#. Use any reasonable browser with javascript enabled to open any URL to the https://rule34.xxx site. + +#. Open your browser's Network Inspector feature (or equivalent). + + .. hint:: + + F12 in Firefox and Chrome + +#. With the inspector open and capturing transactions, complete the captcha test. + +#. Use the network inspector to find the document transaction where the rule34.xxx homepage (or really any page) is returned to you. In the transaction's ``Headers > Request Headers > Cookie`` key. It should contain ``cf_clearance=...`` in its value. Copy and store the total value of the cf_clearance (it should be ~450 characters). + + .. image:: /_static/cf_clearance_example.webp + +#. Use the network inspector to find your browser's ``Headers > Request Headers > User-Agent`` value. Copy and store the total value of this header. + + .. image:: /_static/user-agent_example.webp + +#. Issue your clearance and user-agent to the rule34Py client class using one of the following methods. + + .. important:: + + It is necessary to capture and set both your ``user_agent`` and ``cf_clearance`` value. When Cloudflare issues you a cf_clearance, it is only valid for the User-Agent (of your browser) that completed the captcha test. + + a. In your python module, set the value of ``rule34Py.cf_clearance`` to your token value. + + .. code-block:: python + + import rule34Py as r34 + + client = r34.rule34Py() + client.cf_clearance = # ${token_value} + client.user_agent = # ${user_agent} + + # Some code that uses the rule34Py client. + + b. Set your execution environment's ``R34_CAPTCHA_CLEARANCE`` variable to your token value. + + .. code-block:: bash + + export R34_CAPTCHA_CLEARANCE=${token_value} + export R34_USER_AGENT=${user_agent} + # some script or commands that use rule34Py + +.. note:: + + Consider that the lifetime of your captcha clearance is the same as in the browser. Do not rely on this feature for multi-day operations or code that you are sharing with others. diff --git a/docs/guides/index.rst b/docs/guides/index.rst new file mode 100644 index 0000000..73ee676 --- /dev/null +++ b/docs/guides/index.rst @@ -0,0 +1,9 @@ +User Guides +=========== + +This section contains how-to guides and special topics of interest to module users and developers. + +.. toctree:: + :maxdepth: 1 + + captcha-clearance diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..67a026f --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,10 @@ +.. mdinclude:: ../README.md + +.. toctree:: + :maxdepth: 2 + :caption: Sections + + tutorials/index + api/index + guides/index + dev/index diff --git a/docs/tutorials/downloader.py b/docs/tutorials/downloader.py new file mode 100644 index 0000000..cc97519 --- /dev/null +++ b/docs/tutorials/downloader.py @@ -0,0 +1,19 @@ +from rule34Py import rule34Py + +client = rule34Py() + +TAGS = ["neko", "sort:score", "-video"] + +results = client.search(tags=TAGS) + +from pathlib import Path +import requests + +DOWNLOAD_LIMIT = 3 + +for result in results[0:DOWNLOAD_LIMIT]: + print(f"Downloading post {result.id} ({result.image}).") + with open(Path(result.image).name, "wb") as fp_output: + resp = requests.get(result.image) + resp.raise_for_status() + fp_output.write(resp.content) diff --git a/docs/tutorials/downloader.rst b/docs/tutorials/downloader.rst new file mode 100644 index 0000000..31259f2 --- /dev/null +++ b/docs/tutorials/downloader.rst @@ -0,0 +1,63 @@ +=============================== +A Simple Rule34 Post Downloader +=============================== + +This tutorial walks you through creating a simple script that searches for and downloads a selection of posts from the `rule34.xxx `_ website using the rule34Py module. + + +1. Install python dependencies from PyPI, using PIP (or your python package manager of choice). + +.. code-block:: bash + + python -m pip install rule34Py + python -m pip install requests + +2. Import the ``rule34Py`` module and instantiate a ``rule34Py`` client object. + +The client brokers interactions with the public Rule34.xxx API endpoints. +It will transparently use the correct endpoint for whatever operation you request. +Data returned by the API will be marshalled into native python data types. + + +.. literalinclude:: downloader.py + :language: python + :lineno-start: 1 + :lines: 1-3 + +3. Use the ``rule34Py.search()`` method to search your tags. + +The ``search()`` method accepts a list of tags. +You can use the same tag syntaxes that are supported on the interactive site's `Searching Cheatsheet `_. + +.. literalinclude:: downloader.py + :language: python + :lineno-start: 5 + :lines: 5-7 + +.. important:: + + In this example, we are excluding posts tagged "video", so that we do not have to handle them specially during the download step. + +.. note:: + + By default, the ``search()`` method will return the first 1000 search results. + You can change this behavior by setting the ``limit`` method parameter. For this example, we will only download the first 3 results, and ignore the remainder. + +4. Download the images and save them to your local disk. + +.. literalinclude:: downloader.py + :language: python + :lineno-start: 9 + :lines: 9- + + +-------------- +Example Script +-------------- + +The complete, example script might look like this. + +.. literalinclude:: downloader.py + :language: python + :linenos: + :name: downloader_py diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst new file mode 100644 index 0000000..a9b98a0 --- /dev/null +++ b/docs/tutorials/index.rst @@ -0,0 +1,9 @@ +Tutorials +========= + +This section contains tutorials for how to use this project. + +.. toctree:: + :maxdepth: 1 + + downloader diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index ee84b0e..0000000 --- a/examples/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Examples -Here you can find some example code for rule34Py. - -## download.py -A simple example script for downloading images/videos \ No newline at end of file diff --git a/examples/downloader.py b/examples/downloader.py deleted file mode 100644 index 5d0d189..0000000 --- a/examples/downloader.py +++ /dev/null @@ -1,33 +0,0 @@ -import requests # request img from web -import shutil # save img locally -import os # just for the file extension in line 24 - -from rule34Py import rule34Py -r34Py = rule34Py() -search = r34Py.search(["nekopara"], limit=3) - -def download(url, file_name): - res = requests.get(url, stream = True) - - # rule34.xxx apis will always return a status - # of 200 (which kinda sucks) - if res.status_code == 200: - - # open a file to write to - with open(file_name,'wb') as f: - - # write the raw body data of the requests - # response to that file - shutil.copyfileobj(res.raw, f) - - print('Image successfully Downloaded: ',file_name) - else: - print('Image Couldn\'t be retrieved') - - - -for result in search: - filename, file_extension = os.path.splitext(result.image) - - # call function to start downloading - download(result.image, str(result.id) + file_extension) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 72c0698..8f124bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,37 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [project.optional-dependencies] +dev = [ + "ruff >= 0.11.10", +] test = [ "pytest >= 8.3.2", "responses >= 0.25.3", ] +docs = [ + "sphinx >= 8.2.3", + "sphinx-mdinclude >= 0.6.2", + "sphinx-rtd-theme >= 3.0.2", +] + +[tool.ruff.lint] +preview = true # necessary to enable the pydoclint rules +select = [ + "D", # pydocstyle + "DOC", # pydoclint +# "I", # isort +# "F", # Pyflakes +# "E", # pycodestyle - error +# "N", # pep8-naming +# "PL", # pylint - all +# "RUF", # Ruff-specific rules +] +ignore = [ + # Ignore line lengths; mostly bc. there is no way to ignore only docstring linelengths. + "E501", + # Ignore PEP 8 bad-module-name. 'rule34Py' does not comply, but will not be renamed. + "N999", +] + +[tool.ruff.lint.pydocstyle] +convention = "google" # Use Google-style docstrings. diff --git a/rule34Py/__init__.py b/rule34Py/__init__.py index 0b19ab3..ab30ca0 100644 --- a/rule34Py/__init__.py +++ b/rule34Py/__init__.py @@ -1,22 +1,21 @@ -"""""" -""" -rule34Py - Python api wrapper for rule34.xxx +# rule34Py - Python api wrapper for rule34.xxx +# +# Copyright (C) 2022-2024 b3yc0d3 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Python api wrapper for rule34.xxx.""" -Copyright (C) 2022-2024 b3yc0d3 - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" from rule34Py.rule34 import rule34Py from rule34Py.icame import ICame diff --git a/rule34Py/__vars__.py b/rule34Py/__vars__.py index 852b2de..d232804 100644 --- a/rule34Py/__vars__.py +++ b/rule34Py/__vars__.py @@ -1,22 +1,21 @@ -"""""" -""" -rule34Py - Python api wrapper for rule34.xxx +# rule34Py - Python api wrapper for rule34.xxx +# +# Copyright (C) 2022-2024 b3yc0d3 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""Define module-scoped, hidden constants and variables.""" -Copyright (C) 2022-2024 b3yc0d3 - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" __version__tuple__ = ("2", "1", "0") __author__ = ("b3yc0d3") diff --git a/rule34Py/api_urls.py b/rule34Py/api_urls.py index 834dbed..6a7ee75 100644 --- a/rule34Py/api_urls.py +++ b/rule34Py/api_urls.py @@ -1,40 +1,47 @@ -"""""" -""" -rule34Py - Python api wrapper for rule34.xxx +# rule34Py - Python api wrapper for rule34.xxx +# +# Copyright (C) 2022 Tal A. Baskin +# Copyright (C) 2023-2024 b3yc0d3 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""A module containing URL templates for the rule34.xxx websites.""" -Copyright (C) 2022 Tal A. Baskin -Copyright (C) 2023-2024 b3yc0d3 - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" from enum import Enum from rule34Py.__vars__ import __base_url__, __api_url__ + class API_URLS(str, Enum): - """ - Api Urls + """rule34.xxx API endpoint URLs. Internal class used to change easily urls, if they should ever change. """ - - SEARCH = f"{__api_url__}index.php?page=dapi&s=post&q=index&limit={{LIMIT}}&tags={{TAGS}}&json=1" # returns: JSON - COMMENTS = f"{__api_url__}index.php?page=dapi&s=comment&q=index&post_id={{POST_ID}}" # returns: XML - USER_FAVORITES = f"{__api_url__}index.php?page=favorites&s=view&id={{USR_ID}}" # returns: HTML - GET_POST = f"{__api_url__}index.php?page=dapi&s=post&q=index&id={{POST_ID}}&json=1" # returns: JSON - ICAME = f"{__base_url__}index.php?page=icame" # returns: HTML - RANDOM_POST = f"{__base_url__}index.php?page=post&s=random" # returns: HTML - USER_PAGE = f"{__api_url__}index.php?page=account&s=profile&id={{USER_ID}}" # returns: HTML - POOL = f"{__base_url__}index.php?page=pool&s=show&id={{POOL_ID}}" # returns: HTML + #: The JSON search endpoint. + SEARCH = f"{__api_url__}index.php?page=dapi&s=post&q=index&limit={{LIMIT}}&tags={{TAGS}}&json=1" + #: The XML Post comments endpoint. + COMMENTS = f"{__api_url__}index.php?page=dapi&s=comment&q=index&post_id={{POST_ID}}" + #: An HTML User favorites endpoint. + USER_FAVORITES = f"{__api_url__}index.php?page=favorites&s=view&id={{USR_ID}}" + #: The JSON Post endpoint. + GET_POST = f"{__api_url__}index.php?page=dapi&s=post&q=index&id={{POST_ID}}&json=1" + #: The HTML ICAME page URL. + ICAME = f"{__base_url__}index.php?page=icame" + #: The HTML Random post URL. + RANDOM_POST = f"{__base_url__}index.php?page=post&s=random" + #: An HTML User profile URL. + USER_PAGE = f"{__api_url__}index.php?page=account&s=profile&id={{USER_ID}}" + #: An HTML Pool URL. + POOL = f"{__base_url__}index.php?page=pool&s=show&id={{POOL_ID}}" + #: The HTML toptags URL. TOPMAP = f"{__base_url__}index.php?page=toptags" diff --git a/rule34Py/html.py b/rule34Py/html.py index 04f75bf..7a44b9e 100644 --- a/rule34Py/html.py +++ b/rule34Py/html.py @@ -7,29 +7,40 @@ from bs4 import BeautifulSoup from rule34Py.icame import ICame -from rule34Py.toptag import TopTag from rule34Py.pool import Pool, PoolHistoryEvent +from rule34Py.toptag import TopTag class ICamePage(): """The Rule34 'icame' page. - + + https://rule34.xxx/index.php?page=icame + This class can be instantiated as an object that automatically parses the useful information from the page's html, or used as a static class to parse the page's html directly. + + Args: + html: The page HTML to parse. """ + #: The top icame results, in descending order. + #: ie. element 0 is the most popular tag. top_chart: list[ICame] = [] def __init__(self, html: str): + """Create a new ICamePage object.""" self.top_chart = ICamePage.top_chart_from_html(html) @staticmethod def top_chart_from_html(html: str) -> list[ICame]: """Parse the ICame Top 100 chart from the page html. - - :returns: A list of the top 100 ICame characters and their counts. - :rtype: list[ICame] + + Args: + html: The ICame page HTML, as a string. + + Returns: + A list of the top 100 ICame characters and their counts. """ e_doc = BeautifulSoup(html, features="html.parser") @@ -48,12 +59,18 @@ def top_chart_from_html(html: str) -> list[ICame]: class PoolHistoryPage(): - """A Rule34 Pool history page. - """ + """A Rule34 Pool history page.""" @staticmethod def events_from_html(html: str) -> list[PoolHistoryEvent]: - """Parse the history event entries from the page html.""" + """Parse the history event entries from the page html. + + Args: + html: The pool history page HTML to parse. + + Returns: + A list of ``PoolHistoryEvents`` representing the changes in this Pool. + """ e_doc = BeautifulSoup(html, "html.parser") events = [] @@ -78,16 +95,24 @@ def events_from_html(html: str) -> list[PoolHistoryEvent]: class PoolPage(): """A Rule34 post Pool page.""" + #: Regex matcher for the pool's ID. RE_PAGE_ID = re.compile(r"id=(\d+)") @staticmethod def pool_from_html(html: str) -> Pool: - """Generate a Pool object from the page HTML.""" + """Generate a Pool object from the page HTML. + + Args: + html: The pool page HTML, as a string. + + Returns: + A Pool object representing the parsed page information. + """ e_doc = BeautifulSoup(html, "html.parser") e_content = e_doc.find("div", id="content") # The pool html does not explicitly describe the pool ID anywhere, so - # extract it implictly from the pool history link href. + # extract it implicitly from the pool history link href. e_subnavbar = e_doc.find("ul", id="subnavbar") e_history = e_subnavbar.find_all("a")[-1] # last link on the bar assert e_history.text == "History" # check for safety @@ -113,26 +138,42 @@ def pool_from_html(html: str) -> Pool: return pool - class TagMapPage(): """The rule34.xxx/static/tagmap.html page. This class can be instantiated as an object that automatically parses the useful information from the page's html, or used as a static class to parse the page's html directly. + + Args: + html: The Tag Map page HTML, as a string. """ + #: Regex matcher for the TagMap page's plotly plot RE_NEWPLOT = re.compile(r"newPlot\((.*)\)", flags=re.S) + #: Regex matcher for each plotly point on the tag map RE_PLOT_POINT = re.compile(r"({[^}]+})", flags=re.S) + #: The JSON keys of the data points embedded in the plotly block MAP_POINT_KEYS = {"locationmode", "locations", "text"} + #: The most popular tag in each country or region code. + #: Formatted as ``[location_code, top_tag]``. map_points: dict[str, str] = {} def __init__(self, html: str): + """Create a new TagMapPage from the page's HTML.""" self.map_points = TagMapPage.map_points_from_html(html) @staticmethod def map_points_from_html(html: str) -> dict[str, str]: + """Parse the map data from a Tag Map Page. + + Args: + html: The Tag Map page HTML as a string. + + Returns: + The map data as a dictionary of ``[location_code, top_tag]``. + """ e_doc = BeautifulSoup(html, "html.parser") map_points = {} @@ -160,19 +201,27 @@ class TopTagsPage(): This class can be instantiated as an object that automatically parses the useful information from the page's html, or used as a static class to parse the page's html directly. + + Args: + html: The Top Tags page HTML, as a string. """ + #: The top-tags ranking list on this page. top_tags: list[TopTag] = [] def __init__(self, html: str): + """Create a new TopTagsPage from the page's HTML.""" self.top_tags = TopTagsPage.top_tags_from_html(html) - + @staticmethod def top_tags_from_html(html: str) -> list[TopTag]: """Parse the "Top 100 tags, global" table from the page. - - :returns: A list of TopTags representing the top 100 chart. - :rtype: list[TopTag] + + Args: + html: The Top Tags page HTML, as a string. + + Returns: + A list of TopTags representing the top 100 chart. """ e_doc = BeautifulSoup(html, features="html.parser") e_rows = e_doc.find("table", class_="server-assigns").find_all("tr") diff --git a/rule34Py/icame.py b/rule34Py/icame.py index d4e6c8c..ee79556 100644 --- a/rule34Py/icame.py +++ b/rule34Py/icame.py @@ -1,72 +1,55 @@ -"""""" +# rule34Py - Python api wrapper for rule34.xxx +# +# Copyright (C) 2022-2024 b3yc0d3 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""A module containing content related to the rule34.xxx iCame popularity contest. + +https://rule34.xxx/index.php?page=icame """ -rule34Py - Python api wrapper for rule34.xxx -Copyright (C) 2022-2024 b3yc0d3 - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" class ICame: - """ - ICame chart item + """An iCame contestant. + + Represents an entry on the Rule34.xxx iCame top 100 rankings. """ - def __init__(self, character_name: str, count: int): - """ - iCame chart item. - - :param character_name: Name of the character. - :type character_name: str + def __init__(self, character_name: str, count: int) -> "ICame": + """An iCame contestant. - :param count: Count of how often people came on the character. - :type count: int + Args: + character_name: The name of the character. + May be an empty string, representing posts that have no tagged character. + count: The 'iCame(TM) count'. + A count of how often people came on the character. """ - self._character_name = character_name self._tag_url = "https://rule34.xxx/index.php?page=post&s=list&tags={0}".format(character_name.replace(" ", "_")) self._count = count @property def character_name(self) -> str: - """ - Get name of character. - - :return: Name of character. - :rtype: str - """ - + """The name of the character.""" return self._character_name @property def tag_url(self) -> str: - """ - Get url of tag. - - :return: Url of tag. - :rtype: str - """ - + """The character tag page URL.""" return self._tag_url @property def count(self) -> int: - """ - Get count of how often people came on the character. - - :return: Cum count. - :rtype: int - """ - + """A count of how often people came on the character.""" return self._count diff --git a/rule34Py/pool.py b/rule34Py/pool.py index 0b5dbe7..0fd98bb 100644 --- a/rule34Py/pool.py +++ b/rule34Py/pool.py @@ -6,20 +6,41 @@ @dataclass class PoolHistoryEvent(): - """A Rule34 Pool history record.""" + """A Rule34 Pool history record. + Parameters: + date: The datetime of the historical event. + updater_uname: The user name who initiated the event. + post_ids: A list of the Pool posts that were affected by the event. + """ + + #: The datetime of the pool history event. date: datetime + #: The user name who initiated the event. updater_uname: str + + #: A list of the Pool posts that were affected by the event. post_ids: list[int] = field(default_factory=list()) @dataclass class Pool(): - """A collection of Rule34 Posts.""" + """A collection of Rule34 Posts. + + Parameters: + pool_id: The pool's unique numeric ID. + name: The pool's title. + description: A summary description of the pool's contents. + posts: A list of Post ID numbers that are members of the pool. + """ + #: The pool's unique numeric ID. pool_id: int + #: The pool's title. name: str + #: A summary description of the pool's contents. description: str + #: A list of Post ID numbers that are members of the pool. posts: list[int] = field(default_factory = list) diff --git a/rule34Py/post.py b/rule34Py/post.py index 0361653..2248a72 100644 --- a/rule34Py/post.py +++ b/rule34Py/post.py @@ -1,31 +1,55 @@ -"""""" -""" -rule34Py - Python api wrapper for rule34.xxx +# rule34Py - Python api wrapper for rule34.xxx +# +# Copyright (C) 2022-2024 b3yc0d3 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""A module for representing Rule34 Post objects.""" + +# TODO: Restructure internal variable names -Copyright (C) 2022-2024 b3yc0d3 - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -""" -TODO: Restructure internal variable names -""" class Post: - + """A Rule34 Post object. + + This class is mostly a pythonic representation of the Rule34.xxx JSON API post object. + + Parameters: + id: The Post's Rule34 ID number. + hash: The Post's Rule34 object hash. + score: The Post's voted user score. + size: A two-element list of image dimensions. [width, height] + image: URL to the Post's rule34 image server location. + preview: A URL to the Post's preview image. + sample: A URL to the Post's sample image. + owner: The user who owns the Post. + tags: A list of tags to assign to the Post. + file_type: The Post's image file type. One of ["image", "gif", "video"]. + directory: The Post's image directory on the Rule34 image server. + change: The Post's change ID. + """ + @staticmethod - def from_json(json): + def from_json(json: str) -> "Post": + """Initialize a Post object from an ``api.rule34.xxx`` JSON Post element. + + Args: + json: The JSON string to parse. + + Returns: + The rule34Py.Post representation of the object. + """ pFileUrl = json["file_url"] pHash = json["hash"] pId = json["id"] @@ -34,7 +58,7 @@ def from_json(json): pOwner = json["owner"] pTags = json["tags"].split(" ") preview = json["preview_url"] - sample = json["sample_url"] # thumbnail + sample = json["sample_url"] change = json["change"] directory = json["directory"] @@ -43,6 +67,7 @@ def from_json(json): return Post(pId, pHash, pScore, pSize, pFileUrl, preview, sample, pOwner, pTags, img_type, directory, change) def __init__(self, id: int, hash: str, score: int, size: list, image: str, preview: str, sample: str, owner: str, tags: list, file_type: str, directory: int, change: int): + """Create a new Post object.""" self._file_type = file_type self._video = "" self._image = "" @@ -55,11 +80,11 @@ def __init__(self, id: int, hash: str, score: int, size: list, image: str, previ self._id = id self._hash = hash self._score = score - self._size = size # > [WIDTH:int, HEIGHT:int] - self._preview = preview # thumbnail + self._size = size + self._preview = preview self._sample = sample self._owner = owner - self._tags = tags # > [TAG:str, TAG:str,...] + self._tags = tags self._directory = directory self._change = change self._rating = None @@ -67,162 +92,134 @@ def __init__(self, id: int, hash: str, score: int, size: list, image: str, previ @property def id(self) -> int: - """ - Obtain the unique identifier of post. + """The unique numeric identifier of post. - :return: The unique identifier associated with the post. - :rtype: int + Returns: + The unique numeric identifier associated with the post. """ return self._id @property def hash(self) -> str: - """ - Obtain the unique hash of post. + """The unique rule34 hash of post. - :return: The hash associated with the post. - :rtype: str + Returns: + The hash associated with the post. """ return self._hash @property def score(self) -> int: - """ - Get the score of post. + """Get the score of post. - :return: The post's score. - :rtype: int + Returns: + The post's score. """ return self._score @property - def size(self) -> list: - """ - Retrieve actual size of post's image. + def size(self) -> list[int]: + """The Post's graphical dimension size. - :return: List of [width, height] representing the image dimensions. - :rtype: list[int, int] + Returns: + A list of the image's graphical dimensions, as [width, height]. """ return self._size @property def rating(self) -> str: - """ - Retrieve the content rating of the post. + """The Post's content objectionability rating. - :return: A string representing the post's rating. - - 'e' Explicit - - 's' Safe - - 'q' Questionable - :rtype: str + Returns: + A string representing the post's rating. + - ``e`` = Explicit + - ``s`` = Safe + - ``q`` = Questionable """ return self._rating @property def image(self) -> str: - """ - Get the image of the post. + """The Post's full-resolution image URL. - :return: Image url for the post. - :rtype: str + Returns: + A string URL to the full-resolution image of the Post. """ - return self._image @property def video(self) -> str: - """ - Get the video of the post. + """The Post's full-resolution video URL. - :return: Video url for the post. - :rtype: str + Returns: + A string URL to the full-resolution video URL. """ - return self._video @property def thumbnail(self) -> str: - """ - Get the thumbnail image of the post. + """The Post's thumbnail image URL. - :return: Thumbnail url for the post. - :rtype: str + Returns: + A string URL to the Post's thumbnail image. """ - return self._preview @property def sample(self) -> str: - """ - Get the sample image/video of the post. + """The Post's sample image URL. - :return: Sample data url for the post. - :rtype: str + Returns: + A string URL to the sample image. """ - return self._sample @property def owner(self) -> str: - """ - Get username of post creator. - - :return: Username of post creator. - :rtype: str - """ + """The Post's creator username. + Returns: + The string username of the Post creator. + """ return self._owner @property def tags(self) -> list: - """ - Get tags of post. + """The Post's tags. - :return: List of posts tags - :rtype: list[str] + Returns: + A List of the Post's tags. """ - return self._tags @property def content_type(self) -> str: - """ - Get type of post data (e.g. gif, image etc.) + """The Post's content type. - Represents the value of ``file_type`` from the api. - - :return: A string indicating the type of the post. - Possible values: - - - 'image': Post is of an image. - - 'gif': Post is of an animation (gif, webm, or other format). - - 'video': Post is of a video. - :rtype: str + Represents the value of ``file_type`` from the JSON. + + Returns: + A string indicating the Post's content type. + - ``image``: A static image. + - ``gif``: An animated image (gif, webm, or other format). + - ``video``: A video file. """ - return self._file_type - + @property def change(self) -> int: - """ - Post last update time - - Retrieve the timestamp indicating the last update/change of - the post, as unix time epoch. + """The Post's latest update time. - :return: UNIX Timestamp representing the post's last update/change. - :rtype: int + Returns: + An int representing the UNIX timestamp of the Post's latest update/change. """ - return self._change - + @property def directory(self) -> int: - """ - Get directory id of post + """The Post's storage directory id. - :return: Unknown Data - :rtype: int + Returns: + A numeric representation of the Post's storage directory. """ - - return self._directory \ No newline at end of file + return self._directory diff --git a/rule34Py/post_comment.py b/rule34Py/post_comment.py index 2720e7b..a8ddcee 100644 --- a/rule34Py/post_comment.py +++ b/rule34Py/post_comment.py @@ -1,54 +1,40 @@ -"""""" -""" -rule34Py - Python api wrapper for rule34.xxx +# rule34Py - Python api wrapper for rule34.xxx +# +# Copyright (C) 2022-2024 b3yc0d3 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""A module providing classes representing Rule34 Post comments.""" -Copyright (C) 2022-2024 b3yc0d3 - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" class PostComment: - """ - PostComment - - Class to represent a comment under a post. + """A post comment. + + Args: + id: The comment's numeric ID on Rule34. + owner_id: The numeric ID of the comment author. + body: The comment's body text. + post_id: The numeric ID of the attached post. + creation: The timestamp string of when the post was made. """ def __init__(self, - id: int, - owner_id: int, - body: str, - post_id: int, - creation: str): - """ - Post Comment - - :param id: Comments id. - :type id: int - - :param owner_id: Comment creators id. - :type owner_id: int - - :param body: Content/body of the comment. - :type body: str - - :param post_id: Id of post to whom the comment belongs. - :type post_id: int - - :param creation: Time when the comment was created. - :type creation: str - """ + id: int, + owner_id: int, + body: str, + post_id: int, + creation: str) -> "PostComment": + """Create a new PostComment.""" self._id = id self._owner_id = owner_id self._body = body @@ -57,56 +43,25 @@ def __init__(self, @property def id(self) -> int: - """ - Comments unique id. - - :return: Comments unique id. - :rtype: int - """ + """The comment's unique numeric ID.""" return self._id @property def author_id(self) -> int: - """ - Id of the comments author. - - :return: Id of comment author. - :rtype: int - """ - + """The comment author's unique user ID.""" return self._owner_id @property def body(self) -> str: - """ - Content of the comment. - - :return: Content of the comment. - :rtype: str - """ + """The comment's body text.""" return self._body @property def post_id(self) -> int: - """ - Id of post, to whom the comment belongs. - - :return: Id of parent post. - :rtype: int - """ - + """The numeric ID of the attached post.""" return self._post_id @property def creation(self) -> str: - """ - Timestamp of when the comment was created. - - **Important: currently rule34.xxx api returns the time *when your - api request was made* and _not_ the time when the comment was created.** - - :return: Timestamp when comment was created. - :rtype: str - """ - + """Timestamp string of when the comment was created.""" return self._creation diff --git a/rule34Py/rule34.py b/rule34Py/rule34.py index a1e3780..af442ca 100644 --- a/rule34Py/rule34.py +++ b/rule34Py/rule34.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""This module contains the top-level Rule34 API client class.""" +"""A module containing the top-level Rule34 API client class.""" from collections.abc import Iterator from urllib.parse import parse_qs @@ -41,44 +41,51 @@ from rule34Py.toptag import TopTag -DEFAULT_USER_AGENT = f"Mozilla/5.0 (compatible; rule34Py/{__version__})" -SEARCH_RESULT_MAX = 1000 # API-defined maximum number of search results per request. +#: The default client user_agent, if one is not specified in the environment. +DEFAULT_USER_AGENT: str = f"Mozilla/5.0 (compatible; rule34Py/{__version__})" +#: API-defined maximum number of search results per request. +#: [`Rule34 Docs `_] +SEARCH_RESULT_MAX: int = 1000 class rule34Py(): """The rule34.xxx API client. - Usage: - ```python - client = rule34Py() - post = client.get_post(1234) - ``` + This class is the primary broker for interactions with the real Rule34 servers. + It transparently chooses to interact with either the REST server endpoint, or the PHP interactive site, depending on what methods are executed. + + Example: + .. code-block:: python + + client = rule34Py() + post = client.get_post(1234) """ - CAPTCHA_COOKIE_KEY: str = "cf_clearance" + CAPTCHA_COOKIE_KEY: str = "cf_clearance" #: Captcha clearance HTML cookie key _base_site_rate_limiter = LimiterAdapter(per_second=1) + #: Client captcha clearance token. Defaults to either the value of the ``R34_CAPTCHA_CLEARANCE`` environment variable; or None, if the environment variable is not asserted. + #: Can be overridden at runtime by the user. captcha_clearance: str | None = os.environ.get("R34_CAPTCHA_CLEARANCE", None) + #: The ``requests.Session`` object used when the client makes HTML requests. session: requests.Session = None + #: The ``User-Agent`` HTML header value used when the client makes HTML requests. + #: Defaults to either the value of the ``R34_USER_AGENT`` environment variable; or the ``rule34Py.rule34.DEFAULT_USER_AGENT``, if not asserted. + #: Can be overridden by the user at runtime to change User-Agents. user_agent: str = os.environ.get("R34_USER_AGENT", DEFAULT_USER_AGENT) def __init__(self): - """Initialize a new rule34 API client instance. - - :return: A new rule34 API client instance. - :rtype: rule34Py - """ + """Initialize a new rule34 API client instance.""" self.session = requests.session() self.session.mount(__base_url__, self._base_site_rate_limiter) def _get(self, *args, **kwargs) -> requests.Response: """Send an HTTP GET request. - This method largely passes its arguments to the requests.get() method, - while also inserting a valid User-Agent. + This method largely passes its arguments to the `requests.session.get() `_ method, while also inserting a valid User-Agent and captcha clearance. - :return: A requests.Response object from the GET request. - :rtype: requests.Response + Returns: + The Response object from the GET request. """ # headers kwargs.setdefault("headers", {}) @@ -91,16 +98,22 @@ def _get(self, *args, **kwargs) -> requests.Response: return self.session.get(*args, **kwargs) - def get_comments(self, post_id: int) -> list: - """Retrieve comments of post by its ID. + def get_comments(self, post_id: int) -> list[PostComment]: + """Retrieve the comments left on a post. - :param post_id: The Post's ID number. - :type post_id: int + Args: + post_id: The Post's ID number. - :return: List of comments. - :rtype: list[PostComment] - """ + Error: + Due to a bug in the rule34 site API, the creation timestamp in returned comments are erroneously set to the time that the comment API request is received, not the comments' true creation times. + Returns: + List of comments returned from the request. + If the post has no comments, an empty list will be returned. + + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 params = [ ["POST_ID", str(post_id)] ] @@ -123,17 +136,20 @@ def get_comments(self, post_id: int) -> list: return comments def get_pool(self, pool_id: int) -> Pool: - """Retrieve a pool of Posts by its pool ID. + """Retrieve a pool of Posts. - **Be aware that if "fast" is set to False, it may takes longer.** + Note: + This method uses the interactive website and is rate-limited. - :param pool_id: Pools id. - :type pool_id: int + Args: + pool_id: The pool's object ID on Rule34. - :return: A Pool object representing the requested pool. - :rtype: Pool - """ + Returns: + A Pool object representing the requested pool. + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 params = [ ["POOL_ID", str(pool_id)] ] @@ -144,12 +160,15 @@ def get_pool(self, pool_id: int) -> Pool: def get_post(self, post_id: int) -> Post | None: """Get a Post by its ID. - :param post_id: The Post's ID number. - :type post_id: int + Args: + post_id: The Post's Rule34 ID. - :return: The Post object matching the post_id; or None, if the post_id is not found. - :rtype: Post | None - """ + Returns: + The Post object matching the post_id; or None, if the post_id is not found. + + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 params = [ ["POST_ID", str(post_id)] ] @@ -164,12 +183,18 @@ def get_post(self, post_id: int) -> Post | None: post_json = response.json() return Post.from_json(post_json[0]) - def icame(self) -> list: - """Retrieve list of top 100 iCame list. + def icame(self) -> list["ICame"]: + """Retrieve a list of the top 100 iCame objects. - :return: List of iCame objects. - :rtype: list[ICame] - """ + Note: + This method uses the interactive website and is rate-limited. + + Returns: + The current top 100 iCame objects. + + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 response = self._get(API_URLS.ICAME.value) response.raise_for_status() return ICamePage.top_chart_from_html(response.text) @@ -181,17 +206,21 @@ def iter_search( ) -> Iterator[Post]: """Iterate through Post search results, one element at a time. - This method transparently requests additional results pages until either max_results is reached, or there are no more results. It is possible that additional Posts may be added to the results between page calls, and so it is recommended that you deduplicate results if that is important to you. + This method transparently requests additional results pages until either ``max_results`` is reached, or there are no more results. + It is possible that additional Posts may be added to the results between page calls, and so it is recommended that you deduplicate results if that is important to you. - :param tags: Tag list to search. - :type tags: list[str] + Args: + tags: A list of tags to search. + If the tags list is empty, all posts will be returned. + max_results: The maximum number of results to return before ending the iteration. + If ``None``, then iteration will continue until the end of the results. - :param max_results: The maximum number of results to return before ending the iteration. If 'None', then iteration will continue until the end of the results. Defaults to 'None'. - :type max_results: int|None + Yields: + An iterator representing each Post element of the search results. - :return: Yields a Post Iterator. - :rtype: Iterator[Post] - """ + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 page_id = 0 # what page of the search results we're on results_count = 0 # accumulator of how many results have been returned @@ -207,19 +236,23 @@ def iter_search( page_id += 1 def _parseUrlParams(self, url: str, params: list) -> str: - """ - Parse url parameters. + """Parse url parameters. - **This function is only used internally.** + Args: + url: URL, containing placeholder values. + params: A list of parameter values, to substitute into the URL's placeholders. - :return: Url filed with filled in placeholders. - :rtype: str + Example: + .. code-block:: python - :Example: - self._parseUrlParams("domain.com/index.php?v={{VERSION}}", [["VERSION", "1.10"]]) - """ + formatted_url = _parseUrlParams( + "domain.com/index.php?v={{VERSION}}", + [["VERSION", "1.10"]] + ) - # Usage: _parseUrlParams("domain.com/index.php?v={{VERSION}}", [["VERSION", "1.10"]]) + Returns: + The input URL, reformatted to contain the parameters in place of their placeholders. + """ retURL = url for g in params: @@ -235,21 +268,32 @@ def random_post(self) -> Post: This method behaves similarly to the website's Post > Random function. - :return: Post object. - :rtype: Post - """ + Note: + This method uses the interactive website and is rate-limited. + + Returns: + A random Post. + + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 return self.get_post(self.random_post_id()) def random_post_id(self) -> int: """Get a random Post ID. This method returns the Post ID contained in the 302 redirect the - website responds with, when you request use random post function. + website responds with, when you use the "random post" function. - :return: A random Post ID. - :rtype: int - """ + Note: + This method uses the interactive website and is rate-limited. + + Returns: + A random post ID. + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 response = self._get(API_URLS.RANDOM_POST.value) response.raise_for_status() parsed = urlparse.urlparse(response.url) @@ -257,27 +301,28 @@ def random_post_id(self) -> int: def search(self, tags: list[str] = [], - page_id: int = None, + page_id: int | None = None, limit: int = SEARCH_RESULT_MAX, ) -> list[Post]: """Search for posts. - :param tags: List of tags. - :type tags: list[str] - - :param page_id: Page number/id. - :type page_id: int - - :param limit: Limit for posts returned per page (max. 1000). - :type limit: int + Args: + tags: A list of tags to search for. + page_id: The search page number to request, or None. + If None, search will eventually return all pages. + limit: The maximum number of post results to return per page. + Defaults to ``SEARCH_RESULT_MAX`` (1000, by Rule34 policy). - :return: List of Post objects for matching posts. - :rtype: list[Post] + Returns: + A list of Post objects, representing the search results. - For more information, see: + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + ValueError: An invalid ``limit`` value was requested. + References: - `rule34.xxx API Documentation `_ - """ + """ # noqa: DOC502 if limit < 0 or limit > SEARCH_RESULT_MAX: raise ValueError(f"Search limit must be between 0 and {SEARCH_RESULT_MAX}.") @@ -316,11 +361,16 @@ def set_base_site_rate_limit(self, enabled: bool): def tag_map(self) -> dict[str, str]: """Retrieve the tag map points. - This method uses the tagmap static HTML. - - :return: A mapping of country and district codes to their top tag. 3-letter keys are ISO-3 character country codes, 2-letter keys are US-state codes. - :rtype: dict[str, str] - """ + Note: + This method uses the interactive website and is rate-limited. + + Returns: + A mapping of country and district codes to their top tag. + 3-letter keys are ISO-3 character country codes, 2-letter keys are US-state codes. + + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 resp = self._get(__base_url__ + "static/tagmap.html") resp.raise_for_status() return TagMapPage.map_points_from_html(resp.text) @@ -328,11 +378,18 @@ def tag_map(self) -> dict[str, str]: def tagmap(self) -> list[TopTag]: """Retrieve list of top 100 global tags. - This method is deprecated in favor of the top_tags() method. + Warning: + This method is deprecated. - :return: List of top 100 tags, globally. - :rtype: list[TopTag] - """ + Warn: + This method is deprecated in favor of the top_tags() method. + + Returns: + A list of the current top 100 tags, globally. + + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 warnings.warn( "The rule34Py.tagmap() method is scheduled for deprecation in a future release. If you want to retrieve the Global Top-100 tags list, use the rule34Py.top_tags() method. If you want to retrieve the tag map data points, use the rule34Py.tag_map() method (with an underscore.). See `https://github.com/b3yc0d3/rule34Py/tree/master/docs#functions` for more information.", DeprecationWarning, @@ -342,9 +399,12 @@ def tagmap(self) -> list[TopTag]: def top_tags(self) -> list[TopTag]: """Retrieve list of top 100 global tags. - :return: List of top 100 tags, globally. - :rtype: list[TopTag] - """ + Returns: + A list of the current top 100 tags, globally. + + Raises: + requests.HTTPError: The backing HTTP GET operation failed. + """ # noqa: DOC502 response = self._get(API_URLS.TOPMAP.value) response.raise_for_status() return TopTagsPage.top_tags_from_html(response.text) @@ -353,8 +413,14 @@ def top_tags(self) -> list[TopTag]: def version(self) -> str: """Rule34Py version. - :return: Version of rule34py. - :rtype: str + Warning: + This method is deprecated. + + Warns: + This method is deprecated in favor of rule34Py.version. + + Returns: + The version string of the rule34Py package. """ warnings.warn( "This method is scheduled for deprecation in a future release of rule34Py. Use `rule34Py.version` instead.", diff --git a/rule34Py/toptag.py b/rule34Py/toptag.py index a3bdcf6..43fe450 100644 --- a/rule34Py/toptag.py +++ b/rule34Py/toptag.py @@ -1,84 +1,59 @@ -"""""" +# rule34Py - Python api wrapper for rule34.xxx +# +# Copyright (C) 2022-2024 b3yc0d3 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +"""A module for interacting with the Rule34 TopTag map. + +https://rule34.xxx/index.php?page=toptags """ -rule34Py - Python api wrapper for rule34.xxx -Copyright (C) 2022-2024 b3yc0d3 - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" class TopTag: - """ - TopTag class + """A TopTag entry. + + Parameters: + rank: The popularity rank of the tag. + tagname: The tag name. + percentage: The percentage of global posts that use the tag. """ def __init__(self, rank: int, tagname: str, percentage: int): - """ - TopTag class. - - :param rank: Rank of the tag. - :type rank: int - - :param tagname: Name of the tag. - :type tagname: str - - :param percentage: Percentage of how often tag is used. - :type percentage: int - """ - + """Create a new TopTag class.""" self._rank = rank self._tagname = tagname self._percentage = percentage - - def __from_dict(json: dict): - """ - Create TopTag class from JSON data. - :return: TopTag object. - :rtype: TopTag + def __from_dict(json: dict) -> "TopTag": + """Create a TopTag object from JSON data. + + Returns: + A new TopTag object, populated with values from the ``json`` dictionary. """ return TopTag(json["rank"], json["tagname"], json["percentage"] * 100) - + @property def rank(self) -> int: - """ - Get tags rank. - - :return: Get rank of the tag. - :rtype: int - """ - + """The popularity rank of the tag.""" return self._rank - + @property def tagname(self) -> str: - """ - Get tags name. - - :return: Get name of the tag. - :rytpe: str - """ - + """The tag name.""" return self._tagname - + @property def percentage(self) -> int: - """ - Get tags percentage in use. - - :return: Tags usage as percentage value. - :rtype: int - """ - - return self._percentage \ No newline at end of file + """The percentage of global posts that use the tag.""" + return self._percentage