Repository: yanyongyu/githubkit (language: python)

Issue:
Migrate Documentation to static website



Discussion / hints:

Fix patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 9a934b703..96342fb13 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -17,6 +17,15 @@
             "source.organizeImports": "explicit"
           }
         },
+        "yaml.customTags": [
+          "!ENV scalar",
+          "!ENV sequence",
+          "!relative scalar",
+          "tag:yaml.org,2002:python/name:material.extensions.emoji.to_svg",
+          "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji",
+          "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format",
+          "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping"
+        ],
         "[markdown]": {
           "editor.defaultFormatter": "esbenp.prettier-vscode"
         },
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 000000000..be6910ce9
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,42 @@
+name: Docs CI/CD
+on:
+  push:
+    branches:
+      - master
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Setup Poetry
+        uses: ./.github/actions/setup-poetry
+
+      - uses: actions/cache@v4
+        with:
+          key: mkdocs-material-${{ hashfiles('.cache/**') }}
+          path: .cache
+          restore-keys: |
+            mkdocs-material-
+
+      - name: Build Docs
+        run: |
+          poetry run mkdocs --version
+          poetry run mkdocs build --clean
+          chmod -c -R +rX "site/" | while read line; do
+            echo "::warning title=Invalid file permissions automatically fixed::$line"
+          done
+        env:
+          MKDOCS_GIT_COMMITTERS_APIKEY: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Upload to GitHub Pages
+        uses: actions/upload-pages-artifact@v3
+        with:
+          path: site
+
+      - name: Deploy to GitHub Pages
+        uses: actions/deploy-pages@v4
diff --git a/README.md b/README.md
index 8ab886685..2d2c57c0f 100644
--- a/README.md
+++ b/README.md
@@ -42,919 +42,68 @@ _✨ Always up to date, like octokit ✨_
 
 </div>
 
-## Installation
+<p align="center">
+  <a href="https://yanyongyu.github.io/githubkit/"><b>Documentation</b></a> |
+  <a href="https://github.com/yanyongyu/githubkit/issues"><b>Report Bug</b></a> |
+  <a href="https://docs.github.com/"><b>GitHub Docs</b></a>
+</p>
 
-```bash
-pip install githubkit
-# or, use poetry
-poetry add githubkit
-# or, use pdm
-pdm add githubkit
-# or, use uv
-uv add githubkit
-```
+githubkit aims to be an easy-to-use, fully typed, and always up-to-date GitHub SDK for Python. It is inspired by [octokit](https://github.com/octokit).
 
-if you want to auth as github app, extra dependencies are required:
+githubkit provides several features including:
 
-```bash
-pip install githubkit[auth-app]
-# or, use poetry
-poetry add githubkit[auth-app]
-# or, use pdm
-pdm add githubkit[auth-app]
-# or, use uv
-uv add githubkit[auth-app]
-```
+- Support both sync and async calls
+- Multiple authentication ways and OAuth flow support
+- Calling REST API and GraphQL easily
+- REST API versioning, including GHEC
+- Built-in pagination support
+- Optional data validation with [Pydantic](https://docs.pydantic.dev/latest/), for both webhook events and REST API responses
+- Built-in http cache (powered by [Hishel](https://hishel.com/) for HTTPX) and auto retry
+- Lazy loading of APIs and models
+- Fully typed APIs
+
+## Getting Started
+
+For more, see the [documentation](https://yanyongyu.github.io/githubkit).
 
-if you want to mix sync and async calls in oauth device callback, extra dependencies are required:
+### Installation
+
+Install githubkit with the package manager of your choice:
 
 ```bash
-pip install githubkit[auth-oauth-device]
+pip install githubkit
 # or, use poetry
-poetry add githubkit[auth-oauth-device]
+poetry add githubkit
 # or, use pdm
-pdm add githubkit[auth-oauth-device]
+pdm add githubkit
 # or, use uv
-uv add githubkit[auth-oauth-device]
+uv add githubkit
 ```
 
-githubkit supports **both pydantic v1 and v2**, but pydantic v2 is recommended. If you have encountered any problems with pydantic v1/v2, please file an issue.
-
-> [!WARNING]
-> githubkit uses [GitHub's official openapi schema](https://github.com/github/rest-api-description) to generate apis and models.
-> You may occasionally encounter **breaking changes** like model names or model field types changing when upgrading githubkit.
-> This is due to **upstream schema changes** and githubkit can not control this.
->
-> githubkit recommends using a python dependency manager (like poetry / pdm / uv) to lock the version of githubkit to avoid unexpected changes.
+### Usage
 
-## Quick Start
-
-Here is some common use cases to help you get started quickly. The following examples are written in sync style, you can also use async style by using functions with `async_` prefix. For more detailed usage, please refer to the [Usage](#usage) section.
-
-> APIs are fully typed. Type hints in the following examples are just for reference only.
-
-### Use personal access token (PAT) to call GitHub API
+Create a [Personal Access Token (PAT)](https://github.com/settings/personal-access-tokens/new) and use it to create a `GitHub` instance:
 
 ```python
 from githubkit import GitHub
-from githubkit.versions.latest.models import PublicUser, PrivateUser
 
 github = GitHub("<your_token_here>")
-
-# call GitHub rest api
-resp = github.rest.users.get_authenticated()
-user: PublicUser | PrivateUser = resp.parsed_data
-
-# call GitHub graphql api
-data: dict = github.graphql("{ viewer { login } }")
 ```
 
-### Develop an OAuth APP (GitHub APP) with web flow
-
-OAuth web flow allows you to authenticate as a user and act on behalf of the user.
-
-Note that if you are developing a GitHub APP, you may opt-in / opt-out of the user-to-server token expiration feature. If you opt-in, the user-to-server token will expire after a certain period of time, and you need to use the refresh token to generate a new token. In this case, you need to do more work to handle the token refresh. See [GitHub Docs - Refreshing user access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens) for more information.
-
-If you are developing an OAuth APP or a GitHub APP without user-to-server token expiration:
-
-```python
-from githubkit.versions.latest.models import PublicUser, PrivateUser
-from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy
-
-github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
-
-# redirect user to github oauth page and get the code from callback
-
-# one time usage
-user_github = github.with_auth(github.auth.as_web_user("<code>"))
-
-# or, store the user token in a database for later use
-auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(github)
-# store the user token to database
-access_token = auth.token
-
-# restore the user token from database
-
-user_github = github.with_auth(
-    OAuthTokenAuthStrategy(
-        "<client_id>", "<client_secret>", token=access_token
-    )
-)
-
-# now you can act as the user
-resp = user_github.rest.users.get_authenticated()
-user: PublicUser | PrivateUser = resp.parsed_data
-
-# you can get the user name and id now
-username = user.login
-user_id = user.id
-```
-
-If you are developing a GitHub APP with user-to-server token expiration:
-
-```python
-from githubkit.versions.latest.models import PublicUser, PrivateUser
-from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy
-
-github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
-
-# redirect user to github oauth page and get the code from callback
-
-# one time usage
-user_github = github.with_auth(github.auth.as_web_user("<code>"))
-
-# or, store the user refresh token in a database for later use
-auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(github)
-refresh_token = auth.refresh_token
-
-# restore the user refresh token from database
-
-# you can use the refresh_token to generate a new token
-auth = OAuthTokenAuthStrategy(
-    "<client_id>", "<client_secret>", refresh_token=refresh_token
-)
-# refresh the token manually if you want to store the new refresh token
-# otherwise, the token will be refreshed automatically when you make a request
-auth.refresh(github)
-refresh_token = auth.refresh_token
-
-user_github = github.with_auth(auth)
-
-# now you can act as the user
-resp = user_github.rest.users.get_authenticated()
-user: PublicUser | PrivateUser = resp.parsed_data
-
-# you can get the user name and id now
-username = user.login
-user_id = user.id
-```
-
-### Develop an OAuth APP (GitHub APP) with device flow
-
-```python
-from githubkit import GitHub, OAuthDeviceAuthStrategy, OAuthTokenAuthStrategy
-
-# sync/async func for displaying user code to user
-def callback(data: dict):
-    print(data["user_code"])
-
-user_github = GitHub(OAuthDeviceAuthStrategy("<client_id>", callback))
-
-# if you want to store the user token in a database
-auth: OAuthTokenAuthStrategy = user_github.auth.exchange_token(user_github)
-access_token = auth.token
-refresh_token = auth.refresh_token
-# restore the user token from database
-user_github = user_github.with_auth(
-    OAuthTokenAuthStrategy(
-        "<client_id>", None, refresh_token=refresh_token
-    )
-)
-```
-
-### Develop a GitHub APP
-
-Authenticating as a installation by repository name:
-
-```python
-from githubkit import GitHub, AppAuthStrategy
-from githubkit.versions.latest.models import Issue, Installation
-
-github = GitHub(
-    AppAuthStrategy("your_app_id", "your_private_key", "client_id", "client_secret")
-)
-
-resp = github.rest.apps.get_repo_installation("owner", "repo")
-repo_installation: Installation = resp.parsed_data
-
-installation_github = github.with_auth(
-    github.auth.as_installation(repo_installation.id)
-)
-
-# create a comment on an issue
-resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello")
-issue: IssueComment = resp.parsed_data
-```
-
-Authenticating as a installation by username:
-
-```python
-from githubkit import GitHub, AppAuthStrategy
-from githubkit.versions.latest.models import Installation, IssueComment
-
-github = GitHub(
-    AppAuthStrategy("your_app_id", "your_private_key", "client_id", "client_secret")
-)
-
-resp = github.rest.apps.get_user_installation("username")
-user_installation: Installation = resp.parsed_data
-
-installation_github = github.with_auth(
-    github.auth.as_installation(user_installation.id)
-)
-
-# create a comment on an issue
-resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello")
-issue: IssueComment = resp.parsed_data
-```
-
-## Usage
-
-### Authentication
-
-Initialize a github client with no authentication:
-
-```python
-from githubkit import GitHub, UnauthAuthStrategy
-
-github = GitHub()
-# or, use UnauthAuthStrategy
-github = GitHub(UnauthAuthStrategy())
-```
-
-or using PAT (Token):
-
-```python
-from githubkit import GitHub, TokenAuthStrategy
-
-github = GitHub("<your_token_here>")
-# or, use TokenAuthStrategy
-github = GitHub(TokenAuthStrategy("<your_token_here>"))
-```
-
-or using GitHub APP authentication:
-
-```python
-from githubkit import GitHub, AppAuthStrategy
-
-github = GitHub(
-    AppAuthStrategy(
-        "<app_id>", "<private_key>", "<optional_client_id>", "<optional_client_secret>"
-    )
-)
-```
-
-or using GitHub APP Installation authentication:
-
-```python
-from githubkit import GitHub, AppInstallationAuthStrategy
-
-github = GitHub(
-    AppInstallationAuthStrategy(
-        "<app_id>", "<private_key>", installation_id, "<optional_client_id>", "<optional_client_secret>",
-    )
-)
-```
-
-or using OAuth APP authentication:
-
-```python
-from githubkit import GitHub, OAuthAppAuthStrategy
-
-github = GitHub(OAuthAppAuthStrategy("<client_id_here>", "<client_secret_here>"))
-```
-
-or using GitHub APP / OAuth APP token authentication (This is usefull when you stored the user token in a database):
-
-```python
-from githubkit import GitHub, OAuthTokenAuthStrategy
-
-github = GitHub(
-    OAuthTokenAuthStrategy(
-        "<client_id_here>",
-        "<client_secret_here>",
-        "<access_token_here>",
-        "<access_token_expire_time_here>",
-        "<refresh_token_here>",
-        "<refresh_token_expire_time_here>",
-    )
-)
-```
-
-or using GitHub APP / OAuth APP web flow authentication:
-
-```python
-from githubkit import GitHub, OAuthWebAuthStrategy
-
-github = GitHub(
-    OAuthWebAuthStrategy(
-        "<client_id_here>", "<client_secret_here>", "<web_flow_exchange_code_here>"
-    )
-)
-```
-
-or using GitHub APP / OAuth APP device flow authentication:
-
-```python
-from githubkit import GitHub, OAuthDeviceAuthStrategy
-
-# sync/async func for displaying user code to user
-def callback(data: dict):
-  print(data["user_code"])
-
-github = GitHub(
-    OAuthDeviceAuthStrategy(
-        "<client_id_here>", callback
-    )
-)
-```
-
-See [Switch between AuthStrategy](#switch-between-authstrategy-installation-oauth-webdevice-flow) for more detail about oauth flow.
-
-or using GitHub Action authentication:
-
-```python
-from githubkit import GitHub, ActionAuthStrategy
-
-github = GitHub(ActionAuthStrategy())
-```
-
-and add env or input to the step:
-
-```yaml
-- name: Some step use githubkit
-  with:
-    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
-- name: Some step use githubkit
-  env:
-    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-```
-
-### Config
-
-githubkit is highly configurable, you can change the default config by passing config options to `GitHub`:
-
-```python
-from githubkit import GitHub
-
-github = GitHub(
-    base_url="https://api.github.com/",
-    accept_format="full+json",
-    previews=["starfox"],
-    user_agent="GitHubKit/Python",
-    follow_redirects=True,
-    timeout=None,
-    http_cache=True,
-    auto_retry=True,
-)
-```
-
-The `base_url` option is used to set the base URL of the GitHub API. If you are using GitHub Enterprise Server, you need to include the `/api/v3` path in the base URL.
-
-The `accept_format` and `previews` are used to set the default `Accept` header, you can find more details in [GitHub API docs](https://docs.github.com/en/rest/overview/media-types).
-
-The `http_cache` option enables the http caching feature powered by [Hishel](https://hishel.com/) for HTTPX. GitHub API limits the number of requests that you can make within a specific amount of time. This feature is useful to reduce the number of requests to GitHub API and avoid hitting the rate limit.
-
-The `auto_retry` option enables request retrying when rate limit exceeded and server error encountered. See [Auto Retry](#auto-retry) for more infomation.
-
-### Calling Rest API
-
-> APIs are fully typed. Type hints in the following examples are just for reference only.
-
-Simple sync call:
+Then, enjoy githubkit now!
 
 ```python
 from githubkit import Response
 from githubkit.versions.latest.models import FullRepository
 
-resp: Response[FullRepository] = github.rest.repos.get(owner="owner", repo="repo")
+resp: Response[FullRepository] = github.rest.repos.get("owner", "repo")
 repo: FullRepository = resp.parsed_data
-```
-
-Simple async call:
-
-```python
-from githubkit import Response
-from githubkit.versions.latest.models import FullRepository
-
-resp: Response[FullRepository] = await github.rest.repos.async_get(owner="owner", repo="repo")
-repo: FullRepository = resp.parsed_data
-```
-
-Call API with context (reusing client):
-
-```python
-from githubkit import Response
-from githubkit.versions.latest.models import FullRepository
-
-with GitHub("<your_token_here>") as github:
-    resp: Response[FullRepository] = github.rest.repos.get(owner="owner", repo="repo")
-    repo: FullRepository = resp.parsed_data
-```
-
-```python
-from githubkit import Response
-from githubkit.versions.latest.models import FullRepository
-
-async with GitHub("<your_token_here>") as github:
-    resp: Response[FullRepository] = await github.rest.repos.async_get(owner="owner", repo="repo")
-    repo: FullRepository = resp.parsed_data
-```
-
-> [!WARNING]
-> Note that you should hold a strong reference to the githubkit client instance. Otherwise, githubkit client will fail to call the request.
-> For example, you should not do this:
->
-> ```python
-> from githubkit import GitHub
->
-> def get_client() -> GitHub:
->     return GitHub()
->
-> # This will cause error
-> get_client().rest.repos.get("owner", "repo")
->
-> # This is ok
-> client = get_client()
-> client.rest.repos.get("owner", "repo")
-> ```
-
-### Data Validation
-
-As shown above, the response data is parsed and validated by accessing the `response.parsed_data` property. This ensures that the data type returned by the API is as expected and your code is safe to use it (with static type checking). But sometimes you may want to get the raw data returned by the API, such as when the schema is not correct. You can use the `response.text` property or `response.json()` method to get the raw data:
-
-```python
-from typing import Any, Dict
-from githubkit import Response
-
-resp: Response[FullRepository] = github.rest.repos.get(owner="owner", repo="repo")
-repo: Dict[str, Any] = resp.json()
-```
-
-### Rest API Versioning
-
-> APIs are fully typed. Different versions of APIs are typed separately.
-
-githubkit supports all versions of GitHub API, you can switch between versions as follows:
-
-```python
-github.rest("2022-11-28").repos.get(owner="owner", repo="repo")
-```
-
-The models of versions can be imported from `githubkit.versions.<version>.models`, for example:
-
-```python
-from githubkit.versions.v2022_11_28.models import FullRepository
-```
-
-Specially, the `latest` version is always linked to the latest version of GitHub API:
-
-```python
-from githubkit.versions.latest.models import FullRepository
-```
-
-> [!NOTE]
-> For backward compatibility, the `githubkit.rest` module is linked to the models of `latest` version by default.
->
-> ```python
-> from githubkit.rest import FullRepository
-> ```
-
-You can also get the latest version name of GitHub API and all versions mapping of GitHub API:
-
-```python
-from githubkit.versions import LATEST_VERSION, VERSIONS
-```
-
-Current supported versions are: (you can find it in the section `[[tool.codegen.descriptions]]` of the `pyproject.toml` file)
-
-- 2022-11-28 (latest)
-- ghec-2022-11-28
-
-### Rest API Pagination
-
-Pagination type checking is also supported:
-
-> Typing is tested with Pylance (Pyright).
-
-```python
-from githubkit.versions.latest.models import Issue
-
-for issue in github.paginate(
-    github.rest.issues.list_for_repo, owner="owner", repo="repo", state="open"
-):
-    issue: Issue
-    print(issue.number)
-```
-
-```python
-from githubkit.versions.latest.models import Issue
-
-async for issue in github.paginate(
-    github.rest.issues.async_list_for_repo, owner="owner", repo="repo", state="open"
-):
-    issue: Issue
-    print(issue.number)
-```
-
-complex pagination with custom map function (some api returns data in a nested field):
-
-```python
-async for accessible_repo in github.paginate(
-    github.rest.apps.async_list_installation_repos_for_authenticated_user,
-    map_func=lambda r: r.parsed_data.repositories,
-    installation_id=1,
-):
-    accessible_repo: Repository
-    print(accessible_repo.full_name)
-```
-
-### Calling GraphQL API
-
-Simple sync call:
-
-```python
-data: Dict[str, Any] = github.graphql(query, variables={"foo": "bar"})
-```
-
-Simple async call:
-
-```python
-data: Dict[str, Any] = await github.async_graphql(query, variables={"foo": "bar"})
-```
-
-### GraphQL Pagination
-
-githubkit also provides a helper function to paginate the GraphQL API.
-
-First, You must accept a `cursor` parameter and return a `pageInfo` object in your query. For example:
-
-```graphql
-query ($owner: String!, $repo: String!, $cursor: String) {
-  repository(owner: $owner, name: $repo) {
-    issues(first: 10, after: $cursor) {
-      nodes {
-        number
-      }
-      pageInfo {
-        hasNextPage
-        endCursor
-      }
-    }
-  }
-}
-```
-
-The `pageInfo` object in your query must be one of the following types depending on the direction of the pagination:
-
-For forward pagination, use:
-
-```graphql
-pageInfo {
-  hasNextPage
-  endCursor
-}
-```
-
-For backward pagination, use:
-
-```graphql
-pageInfo {
-  hasPreviousPage
-  startCursor
-}
-```
-
-If you provide all 4 properties in a `pageInfo`, githubkit will default to forward pagination.
-
-Then, you can iterate over the paginated results by using the graphql `paginate` method:
-
-```python
-for result in github.graphql.paginate(
-    query, variables={"owner": "owner", "repo": "repo"}
-):
-    print(result)
-```
-
-Note that the `result` is a dict containing the list of nodes/edges for each page and the `pageInfo` object. You should iterate over the `nodes` or `edges` list to get the actual data. For example:
-
-```python
-for result in g.graphql.paginate(query, {"owner": "owner", "repo": "repo"}):
-    for issue in result["repository"]["issues"]["nodes"]:
-        print(issue)
-```
-
-You can also provide a initial cursor value to start pagination from a specific point:
-
-```python
-for result in github.graphql.paginate(
-    query, variables={"owner": "owner", "repo": "repo", "cursor": "initial_cursor"}
-):
-    print(result)
-```
-
-> [!NOTE]
-> Nested pagination is not supported.
-
-### Auto Retry
-
-By default, githubkit will retry the request when specific exception encountered. When rate limit exceeded, githubkit will retry once after GitHub suggested waiting time. When server error encountered (http status >= 500), githubkit will retry max three times.
-
-You can disable this feature by set the `auto_retry` config to `False`:
-
-```python
-github = GitHub(
-    ...
-    auto_retry=False
-)
-```
-
-You can also customize the retry decision function by passing a callable:
-
-```python
-from datetime import timedelta
-
-from githubkit.retry import RetryOption
-from githubkit.exception import GitHubException
-
-def retry_decision_func(exc: GitHubException, retry_count: int) -> RetryOption:
-    if retry_count < 1:
-        return RetryOption(True, timedelta(seconds=60))
-    return RetryOption(False)
-
-github = GitHub(
-    ...
-    auto_retry=retry_decision_func
-)
-```
-
-githubkit also provides some builtin retry decision function:
-
-1. Retry when rate limit exceeded:
-
-   ```python
-   from githubkit.retry import RETRY_RATE_LIMIT, RetryRateLimit
-
-   # default
-   github = GitHub(
-      ...
-      auto_retry=RETRY_RATE_LIMIT
-   )
-   # or, custom max retry count
-   github = GitHub(
-      ...
-      auto_retry=RetryRateLimit(max_retry=1)
-   )
-   ```
-
-2. Retry when server error encountered:
-
-   ```python
-   from githubkit.retry import RETRY_SERVER_ERROR, RetryServerError
-
-   # default
-   github = GitHub(
-      ...
-      auto_retry=RETRY_SERVER_ERROR
-   )
-   # or, custom max retry count
-   github = GitHub(
-      ...
-      auto_retry=RetryServerError(max_retry=1)
-   )
-   ```
-
-3. Chain retry decision functions:
-
-   ```python
-   from githubkit.retry import RETRY_RATE_LIMIT, RETRY_SERVER_ERROR, RetryChainDecision
-
-   github = GitHub(
-      ...
-      auto_retry=RetryChainDecision(RETRY_RATE_LIMIT, RETRY_SERVER_ERROR)
-   )
-   ```
-
-### Webhook Verification
-
-> `githubkit.webhooks` module contains some shortcut functions to help you verify and parse webhook payload.
-
-Simple webhook payload verification:
-
-```python
-from githubkit.webhooks import verify
-
-valid: bool = verify(secret, request.body, request.headers["X-Hub-Signature-256"])
-```
-
-Sign the webhook payload manually:
-
-```python
-from githubkit.webhooks import sign
-
-signature: str = sign(secret, payload, method="sha256")
-```
-
-### Webhook Parsing
-
-> `githubkit.webhooks` module contains some shortcut functions to help you verify and parse webhook payload.
-
-Parse the payload with event name:
-
-```python
-from githubkit.webhooks import parse
-
-event = parse(request.headers["X-GitHub-Event"], request.body)
-```
-
-(**NOT RECOMMENDED**) Parse the payload without event name (may cost longer time and more memory):
-
-```python
-from githubkit.webhooks import parse_without_name
-
-event = parse_without_name(request.body)
-```
-
-> [!WARNING]
-> The `parse_without_name` function will try to parse the payload with all supported event names.  
-> The behavior of this function is not the same between pydantic v1 and v2.  
-> When using pydantic v1, the function will return the first valid event model (known as `left-to-right` mode).  
-> When using pydantic v2, the function will return the highest scored valid event model (known as `smart` mode).  
-> See: [Union Modes](https://docs.pydantic.dev/latest/concepts/unions/#union-modes).
-
-Parse dict like payload:
-
-```python
-from githubkit.webhooks import parse_obj, parse_obj_without_name
-
-event = parse_obj(request.headers["X-GitHub-Event"], request.json())
-event = parse_obj_without_name(request.json())  # NOT RECOMMENDED
-```
-
-The `parse` and `parse_obj` function supports type overload, if you provide static value for the `event_name` parameter, the return type will be inferred automatically.
-
-Webhook also supports versioning, you can switch between versions as follows:
-
-```python
-from githubkit import GitHub
-
-event = GitHub.webhooks("2022-11-28").parse(request.headers["X-GitHub-Event"], request.body)
-```
-
-### Switch between AuthStrategy (Installation, OAuth Web/Device Flow)
-
-You can change the auth strategy and get a new client simplely using `with_auth`.
-
-Change from `AppAuthStrategy` to `AppInstallationAuthStrategy`:
-
-```python
-from githubkit import GitHub, AppAuthStrategy
-
-github = GitHub(AppAuthStrategy("<app_id>", "<private_key>"))
-installation_github = github.with_auth(
-    github.auth.as_installation(installation_id)
-)
-```
-
-Change from `OAuthAppAuthStrategy` to `OAuthWebAuthStrategy` (OAuth Web Flow):
-
-```python
-from githubkit import GitHub, OAuthAppAuthStrategy
-
-github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
-user_github = github.with_auth(github.auth.as_web_user("<code>"))
-
-# now you can act as the user
-resp = user_github.rest.users.get_authenticated()
-user = resp.parsed_data
-
-# you can get the user token after you maked a request as user
-user_token = user_github.auth.token
-user_token_expire_time = user_github.auth.expire_time
-refresh_token = user_github.auth.refresh_token
-refresh_token_expire_time = user_github.auth.refresh_token_expire_time
-```
-
-you can also get the user token directly without making a request (Change from `OAuthWebAuthStrategy` to `OAuthTokenAuthStrategy`):
-
-```python
-auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(github)
-# or asynchronously
-auth: OAuthTokenAuthStrategy = await github.auth.as_web_user("<code>").async_exchange_token(github)
-user_token = auth.token
-user_token_expire_time = auth.expire_time
-refresh_token = auth.refresh_token
-refresh_token_expire_time = auth.refresh_token_expire_time
-
-user_github = github.with_auth(auth)
-```
-
-Change from `OAuthDeviceAuthStrategy` to `OAuthTokenAuthStrategy`:
-
-```python
-from githubkit import GitHub, OAuthDeviceAuthStrategy
-
-def callback(data: dict):
-    print(data["user_code"])
-
-user_github = GitHub(OAuthDeviceAuthStrategy("<client_id>", callback))
-
-# now you can act as the user
-resp = user_github.rest.users.get_authenticated()
-user = resp.parsed_data
-
-# you can get the user token after you maked a request as user
-user_token = user_github.auth.token
-user_token_expire_time = user_github.auth.expire_time
-refresh_token = user_github.auth.refresh_token
-refresh_token_expire_time = user_github.auth.refresh_token_expire_time
-
-# you can also exchange the token directly without making a request
-auth: OAuthTokenAuthStrategy = github.auth.exchange_token(github)
-# or asynchronously
-auth: OAuthTokenAuthStrategy = await github.auth.async_exchange_token(github)
-user_token = auth.token
-user_token_expire_time = auth.expire_time
-refresh_token = auth.refresh_token
-refresh_token_expire_time = auth.refresh_token_expire_time
-
-user_github = github.with_auth(auth)
-```
-
-## Unit Testing
-
-If you are using githubkit in your business logic, you may want to mock the github API in your unit tests. You can custom the response by mocking the `request`/`arequest` method of the `GitHub` class. Here is an example of how to mock githubkit's API calls:
-
-```python
-import json
-from pathlib import Path
-from typing import Any, Type, Union
-
-import httpx
-import pytest
-
-from githubkit import GitHub
-from githubkit.utils import UNSET
-from githubkit.response import Response
-from githubkit.typing import URLTypes, UnsetType
-from githubkit.versions.latest.models import FullRepository
-
-
-FAKE_RESPONSE = json.loads(Path("fake_response.json").read_text())
-
-
-# Example function you want to test, which calls the GitHub API
-def target_sync_func() -> FullRepository:
-    github = GitHub("xxxxx")
-    resp = github.rest.repos.get("owner", "repo")
-    return resp.parsed_data
-
-
-def mock_request(
-    g: GitHub,
-    method: str,
-    url: URLTypes,
-    *,
-    response_model: Union[Type[Any], UnsetType] = UNSET,
-    **kwargs: Any,  # other request parameters including headers, json, etc.
-) -> Response[Any]:
-    # When the request is made, return a fake response
-    if method == "GET" and url == "/repos/owner/repo":
-        return Response[T](
-            httpx.Response(status_code=200, json=FAKE_RESPONSE),
-            Any if response_model is UNSET else response_model,
-        )
-    raise RuntimeError(f"Unexpected request: {method} {url}")
-
-
-# Test the target function
-def test_sync_mock():
-    with pytest.MonkeyPatch.context() as m:
-        # Patch the request method with the mock
-        m.setattr(GitHub, "request", mock_request)
-
-        repo = target_sync_func()
-        assert isinstance(repo, FullRepository)
+print(repo.full_name)
 ```
 
 ## Development
 
-Open in Codespaces (Dev Container):
-
-[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=512138996)
-
-Generate latest models and apis:
-
-> [!WARNING]
-> This may use about **400M** memory and take a long time.
-
-```bash
-./scripts/run-codegen.sh
-```
-
-Run tests in dev env:
-
-```bash
-./scripts/run-tests.sh
-```
-
-Run tests in test envs, for example:
-
-```bash
-cd ./envs/pydantic-v2/
-poetry run bash ../../scripts/run-tests.sh
-```
+See the [development](https://yanyongyu.github.io/githubkit/contributing/) in the contributing guide.
 
 ## Contributors
 
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 000000000..209f1ebd0
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,65 @@
+<!-- markdownlint-disable MD046 -->
+
+# Contributing
+
+We welcome contributions to the project. Thank you in advance for your contribution to githubkit!
+
+## Development Environment
+
+Open in Codespaces (Dev Container):
+
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=master&repo=512138996)
+
+Local development environment setup:
+
+Make sure you have installed [Poetry](https://python-poetry.org/).
+
+```bash
+poetry install --all-extras --with docs && poetry run pre-commit install
+```
+
+Test environment setup (optional):
+
+```bash
+for env in $(find ./envs/ -maxdepth 1 -mindepth 1 -type d -not -name test); do
+  echo "Setting up $env environment"
+  (cd $env && poetry install --no-root)
+done
+```
+
+## GitHub Schema Update
+
+Generate latest models and apis from GitHub's OpenAPI schema:
+
+!!! warning
+
+    This may use about **400M** memory and take a long time.
+
+Please make sure you have activated the virtual environment.
+
+```bash
+./scripts/run-codegen.sh
+```
+
+### Patch Schema
+
+If you encounter an schema error, you can patch the schema by modifying the `pyproject.toml` file.
+
+In the `[tool.codegen.overrides.schema_overrides]` section, you can modify the schema using json pointer. The value will override the original schema. Specially, if the value is `<unset>`, the original one will be removed.
+
+Please add a comment to explain the reason for the patch if you want to submit a PR.
+
+## Testing
+
+Run tests in dev env:
+
+```bash
+./scripts/run-tests.sh
+```
+
+Run tests in specific test env, for example:
+
+```bash
+cd ./envs/pydantic-v2/
+poetry run bash ../../scripts/run-tests.sh
+```
diff --git a/docs/css/custom.css b/docs/css/custom.css
new file mode 100644
index 000000000..f4d336fdf
--- /dev/null
+++ b/docs/css/custom.css
@@ -0,0 +1,3 @@
+.hidden {
+  display: none;
+}
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 000000000..00b168a0b
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,72 @@
+<!-- markdownlint-disable MD033 MD041 -->
+
+# githubkit {: .hidden }
+
+<div align="center" markdown>
+
+[![githubkit](https://socialify.git.ci/yanyongyu/githubkit/image?description=1&descriptionEditable=%E2%9C%A8%20GitHub%20SDK%20for%20Python%20%E2%9C%A8&font=Bitter&language=1&pattern=Circuit%20Board&theme=Light)](https://github.com/yanyongyu/githubkit)
+
+<p>
+  <a href="https://raw.githubusercontent.com/yanyongyu/githubkit/master/LICENSE">
+    <img src="https://img.shields.io/github/license/yanyongyu/githubkit" alt="license">
+  </a>
+  <a href="https://pypi.python.org/pypi/githubkit">
+    <img src="https://img.shields.io/pypi/v/githubkit?logo=python&logoColor=edb641" alt="pypi">
+  </a>
+  <img src="https://img.shields.io/badge/python-3.8+-blue?logo=python&logoColor=edb641" alt="python">
+  <a href="https://github.com/psf/black">
+    <img src="https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=edb641" alt="black">
+  </a>
+  <a href="https://github.com/Microsoft/pyright">
+    <img src="https://img.shields.io/badge/types-pyright-797952.svg?logo=python&logoColor=edb641" alt="pyright">
+  </a>
+  <a href="https://github.com/astral-sh/ruff">
+    <img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
+  </a>
+  <a href="https://results.pre-commit.ci/latest/github/yanyongyu/githubkit/master">
+    <img src="https://results.pre-commit.ci/badge/github/yanyongyu/githubkit/master.svg" alt="pre-commit" />
+  </a>
+</p>
+
+<!-- markdownlint-capture -->
+<!-- markdownlint-disable MD036 -->
+
+_✨ The modern, all-batteries-included GitHub SDK for Python ✨_
+
+_✨ Support both **sync** and **async** calls, **fully typed** ✨_
+
+_✨ Always up to date, like octokit ✨_
+
+<!-- markdownlint-restore -->
+
+</div>
+
+githubkit aims to be an easy-to-use, fully typed, and always up-to-date GitHub SDK for Python. It is inspired by [octokit](https://github.com/octokit).
+
+githubkit provides several features including:
+
+- :material-multicast: Support both sync and async calls
+- :material-account-key: Multiple authentication ways and OAuth flow support
+- :material-api: Calling REST API and GraphQL easily
+- :material-tag-multiple: REST API versioning, including GHEC
+- :material-page-next: Built-in pagination support
+- :material-database-check: Optional data validation with [Pydantic](https://docs.pydantic.dev/latest/), for both webhook events and REST API responses
+- :material-cached: Built-in http cache (powered by [Hishel](https://hishel.com/) for HTTPX) and auto retry
+- :material-lightning-bolt: Lazy loading of APIs and models
+- :material-check-circle: Fully typed APIs
+
+## Version Changes
+
+githubkit is currently not stable and may have breaking changes in the future. githubkit uses the **minor** version number for breaking changes and the **patch** version number for bug fixes or enhancements.
+
+Note that githubkit uses [GitHub's official openapi schema](https://github.com/github/rest-api-description) to generate apis and models. This aims to keep the APIs and models up-to-date. You may occasionally encounter **breaking changes** like model names or model field types changing when upgrading githubkit. This is due to **upstream github schema changes** and githubkit can not control this.
+
+!!! warning
+
+    githubkit recommends using a python dependency manager (like poetry / pdm / uv) to lock the version of githubkit to avoid unexpected changes.
+
+## Reporting Issues
+
+If you encounter any issues or have some questions, please report them on the [GitHub issue tracker](https://github.com/yanyongyu/githubkit/issues).
+
+If you encounter any schema errors, please provide the reproducible code and the raw response data when reporting the issue. Optionally, you can also report the schema error to upstream [GitHub's official openapi schema](https://github.com/github/rest-api-description) repository.
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 000000000..c7339d8fa
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,113 @@
+# Installation
+
+!!! tip
+
+    githubkit supports **both pydantic v1 and v2**, but pydantic v2 is recommended. If you have encountered any problems with pydantic v1/v2, please file an issue.
+
+## Basic Installation
+
+=== "poetry"
+
+    ```bash
+    poetry add githubkit
+    ```
+
+=== "pdm"
+
+    ```bash
+    pdm add githubkit
+    ```
+
+=== "uv"
+
+    ```bash
+    uv add githubkit
+    ```
+
+=== "pip"
+
+    ```bash
+    pip install githubkit
+    ```
+
+## Extra Dependencies
+
+If you want to auth as github app, you should install `auth-app` extra dependencies:
+
+=== "poetry"
+
+    ```bash
+    poetry add githubkit[auth-app]
+    ```
+
+=== "pdm"
+
+    ```bash
+    pdm add githubkit[auth-app]
+    ```
+
+=== "uv"
+
+    ```bash
+    uv add githubkit[auth-app]
+    ```
+
+=== "pip"
+
+    ```bash
+    pip install githubkit[auth-app]
+    ```
+
+If you want to mix sync and async calls in oauth device callback, you should install `auth-oauth-device` extra dependencies:
+
+=== "poetry"
+
+    ```bash
+    poetry add githubkit[auth-oauth-device]
+    ```
+
+=== "pdm"
+
+    ```bash
+    pdm add githubkit[auth-oauth-device]
+    ```
+
+=== "uv"
+
+    ```bash
+    uv add githubkit[auth-oauth-device]
+    ```
+
+=== "pip"
+
+    ```bash
+    pip install githubkit[auth-oauth-device]
+    ```
+
+## Full Installation
+
+You can install fully featured githubkit with `all` extra dependencies:
+
+=== "poetry"
+
+    ```bash
+    poetry add githubkit[all]
+    ```
+
+=== "pdm"
+
+    ```bash
+    pdm add githubkit[all]
+    ```
+
+=== "uv"
+
+    ```bash
+    uv add githubkit[all]
+    ```
+
+=== "pip"
+
+    ```bash
+    pip install githubkit[all]
+    ```
diff --git a/docs/quickstart/call-api-with-pat.md b/docs/quickstart/call-api-with-pat.md
new file mode 100644
index 000000000..dcb388908
--- /dev/null
+++ b/docs/quickstart/call-api-with-pat.md
@@ -0,0 +1,35 @@
+# Use Personal Access Token (PAT) to Call GitHub API
+
+First, you need to create a PAT at [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new). Then, copy the token and replace `<your_token_here>` in the following code.
+
+## Sync Example
+
+```python
+from githubkit import GitHub
+from githubkit.versions.latest.models import PublicUser, PrivateUser
+
+github = GitHub("<your_token_here>")
+
+# call GitHub rest api
+resp = github.rest.users.get_authenticated()
+user: PublicUser | PrivateUser = resp.parsed_data
+
+# call GitHub graphql api
+data: dict = github.graphql("{ viewer { login } }")
+```
+
+## Async Example
+
+```python
+from githubkit import GitHub
+from githubkit.versions.latest.models import PublicUser, PrivateUser
+
+github = GitHub("<your_token_here>")
+
+# call GitHub rest api
+resp = await github.rest.users.async_get_authenticated()
+user: PublicUser | PrivateUser = resp.parsed_data
+
+# call GitHub graphql api
+data: dict = await github.async_graphql("{ viewer { login } }")
+```
diff --git a/docs/quickstart/github-app.md b/docs/quickstart/github-app.md
new file mode 100644
index 000000000..ff91746d0
--- /dev/null
+++ b/docs/quickstart/github-app.md
@@ -0,0 +1,93 @@
+# Develop a GitHub APP
+
+## Sync Example
+
+Authenticating as a installation by repository name:
+
+```python
+from githubkit import GitHub, AppAuthStrategy
+from githubkit.versions.latest.models import Issue, Installation
+
+github = GitHub(
+    AppAuthStrategy("your_app_id", "your_private_key", "client_id", "client_secret")
+)
+
+resp = github.rest.apps.get_repo_installation("owner", "repo")
+repo_installation: Installation = resp.parsed_data
+
+installation_github = github.with_auth(
+    github.auth.as_installation(repo_installation.id)
+)
+
+# create a comment on an issue
+resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello")
+issue: IssueComment = resp.parsed_data
+```
+
+Authenticating as a installation by username:
+
+```python
+from githubkit import GitHub, AppAuthStrategy
+from githubkit.versions.latest.models import Installation, IssueComment
+
+github = GitHub(
+    AppAuthStrategy("your_app_id", "your_private_key", "client_id", "client_secret")
+)
+
+resp = github.rest.apps.get_user_installation("username")
+user_installation: Installation = resp.parsed_data
+
+installation_github = github.with_auth(
+    github.auth.as_installation(user_installation.id)
+)
+
+# create a comment on an issue
+resp = installation_github.rest.issues.create_comment("owner", "repo", 1, body="Hello")
+issue: IssueComment = resp.parsed_data
+```
+
+## Async Example
+
+Authenticating as a installation by repository name:
+
+```python
+from githubkit import GitHub, AppAuthStrategy
+from githubkit.versions.latest.models import Issue, Installation
+
+github = GitHub(
+    AppAuthStrategy("your_app_id", "your_private_key", "client_id", "client_secret")
+)
+
+resp = await github.rest.apps.async_get_repo_installation("owner", "repo")
+repo_installation: Installation = resp.parsed_data
+
+installation_github = github.with_auth(
+    github.auth.as_installation(repo_installation.id)
+)
+
+# create a comment on an issue
+resp = await installation_github.rest.issues.async_create_comment("owner", "repo", 1, body="Hello")
+issue: IssueComment = resp.parsed_data
+```
+
+Authenticating as a installation by username:
+
+```python
+from githubkit import GitHub, AppAuthStrategy
+from githubkit.versions.latest.models import Installation, IssueComment
+
+github = GitHub(
+    AppAuthStrategy("your_app_id", "your_private_key", "client_id", "client_secret")
+)
+
+resp = await github.rest.apps.async_get_user_installation("username")
+user_installation: Installation = resp.parsed_data
+
+installation_github = github.with_auth(
+    github.auth.as_installation(user_installation.id)
+)
+
+# create a comment on an issue
+resp = await installation_github.rest.issues.async_create_comment("owner", "repo", 1, body="Hello")
+issue: IssueComment = resp.parsed_data
+```
diff --git a/docs/quickstart/index.md b/docs/quickstart/index.md
new file mode 100644
index 000000000..18f45e51a
--- /dev/null
+++ b/docs/quickstart/index.md
@@ -0,0 +1,41 @@
+<!-- markdownlint-disable MD033 MD041 -->
+
+# Quick Start
+
+Here are some common use cases to help you get started quickly. For more detailed usage, please refer to the [Usage](../usage/authentication.md) section.
+
+!!! note
+
+    APIs are fully typed. Type hints in the following examples are just for **reference only**.
+
+<div class="grid cards" markdown>
+
+<!-- prettier-ignore-start -->
+
+- :octicons-file-code-16: [Call API with PAT](./call-api-with-pat.md)
+
+    ---
+
+    Use [Personal Access Token (PAT)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) to call GitHub API
+
+- :octicons-file-code-16: [OAuth APP Web Flow](./oauth-web-flow.md)
+
+    ---
+
+    Develop an [OAuth APP](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) ([GitHub APP](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)) with web flow
+
+- :octicons-file-code-16: [OAuth APP Device Flow](./oauth-device-flow.md)
+
+    ---
+
+    Develop an [OAuth APP](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) ([GitHub APP](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)) with device flow
+
+- :octicons-file-code-16: [GitHub APP](./github-app.md)
+
+    ---
+
+    Develop a [GitHub APP](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)
+
+<!-- prettier-ignore-end -->
+
+</div>
diff --git a/docs/quickstart/oauth-device-flow.md b/docs/quickstart/oauth-device-flow.md
new file mode 100644
index 000000000..475f94bed
--- /dev/null
+++ b/docs/quickstart/oauth-device-flow.md
@@ -0,0 +1,47 @@
+# Develop an OAuth APP (GitHub APP) with device flow
+
+## Sync Example
+
+```python
+from githubkit import GitHub, OAuthDeviceAuthStrategy, OAuthTokenAuthStrategy
+
+# sync/async func for displaying user code to user
+def callback(data: dict):
+    print(data["user_code"])
+
+user_github = GitHub(OAuthDeviceAuthStrategy("<client_id>", callback))
+
+# if you want to store the user token in a database
+auth: OAuthTokenAuthStrategy = user_github.auth.exchange_token(user_github)
+access_token = auth.token
+refresh_token = auth.refresh_token
+# restore the user token from database
+user_github = user_github.with_auth(
+    OAuthTokenAuthStrategy(
+        "<client_id>", None, refresh_token=refresh_token
+    )
+)
+```
+
+## Async Example
+
+```python
+from githubkit import GitHub, OAuthDeviceAuthStrategy, OAuthTokenAuthStrategy
+
+# sync/async func for displaying user code to user
+async def callback(data: dict):
+    print(data["user_code"])
+
+user_github = GitHub(OAuthDeviceAuthStrategy("<client_id>", callback))
+
+# if you want to store the user token in a database
+auth: OAuthTokenAuthStrategy = await user_github.auth.async_exchange_token(user_github)
+access_token = auth.token
+refresh_token = auth.refresh_token
+# restore the user token from database
+user_github = user_github.with_auth(
+    OAuthTokenAuthStrategy(
+        "<client_id>", None, refresh_token=refresh_token
+    )
+)
+```
diff --git a/docs/quickstart/oauth-web-flow.md b/docs/quickstart/oauth-web-flow.md
new file mode 100644
index 000000000..48a7b354f
--- /dev/null
+++ b/docs/quickstart/oauth-web-flow.md
@@ -0,0 +1,159 @@
+# Develop an OAuth APP (GitHub APP) with Web Flow
+
+OAuth web flow allows you to authenticate as a user and act on behalf of the user.
+
+Note that if you are developing a GitHub APP, you may opt-in / opt-out of the user-to-server token expiration feature. If you opt-in, the user-to-server token will expire after a certain period of time, and you need to use the refresh token to generate a new token. In this case, you need to do more work to handle the token refresh. See [GitHub Docs - Refreshing user access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens) for more information.
+
+## Sync Example
+
+If you are developing an OAuth APP or a GitHub APP without user-to-server token expiration:
+
+```python
+from githubkit.versions.latest.models import PublicUser, PrivateUser
+from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy
+
+github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
+
+# redirect user to github oauth page and get the code from callback
+
+user_github = github.with_auth(github.auth.as_web_user("<code>"))  # (1)!
+
+# === or ===
+auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(
+    github
+)  # (2)!
+access_token = auth.token
+
+user_github = github.with_auth(
+    OAuthTokenAuthStrategy("<client_id>", "<client_secret>", token=access_token)
+)  # (3)!
+
+# now you can act as the user
+resp = user_github.rest.users.get_authenticated()
+user: PublicUser | PrivateUser = resp.parsed_data
+
+# you can get the user name and id now
+username = user.login
+user_id = user.id
+```
+
+1. If you just want to use the user token once, simply switch to OAuth Web Auth Strategy.
+2. exchange the user token manually and store it in a database.
+3. restore the user token from database
+
+If you are developing a GitHub APP with user-to-server token expiration:
+
+```python
+from githubkit.versions.latest.models import PublicUser, PrivateUser
+from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy
+
+github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
+
+# redirect user to github oauth page and get the code from callback
+
+user_github = github.with_auth(github.auth.as_web_user("<code>"))  # (1)!
+
+# === or ===
+auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(
+    github
+)  # (2)!
+refresh_token = auth.refresh_token
+
+auth = OAuthTokenAuthStrategy(
+    "<client_id>", "<client_secret>", refresh_token=refresh_token
+)  # (3)!
+auth.refresh(github)  # (4)!
+refresh_token = auth.refresh_token
+
+user_github = github.with_auth(auth)
+
+# now you can act as the user
+resp = user_github.rest.users.get_authenticated()
+user: PublicUser | PrivateUser = resp.parsed_data
+
+# you can get the user name and id now
+username = user.login
+user_id = user.id
+```
+
+1. If you just want to use the user token once, simply switch to OAuth Web Auth Strategy.
+2. exchange the user token manually and store it in a database.
+3. restore the user refresh token from database and generate a new token.
+4. refresh the token manually and store the new one. otherwise, the token will be refreshed automatically when you make a request.
+
+## Async Example
+
+If you are developing an OAuth APP or a GitHub APP without user-to-server token expiration:
+
+```python
+from githubkit.versions.latest.models import PublicUser, PrivateUser
+from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy
+
+github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
+
+# redirect user to github oauth page and get the code from callback
+
+user_github = github.with_auth(github.auth.as_web_user("<code>"))  # (1)!
+
+# === or ===
+auth: OAuthTokenAuthStrategy = await github.auth.as_web_user(
+    "<code>"
+).async_exchange_token(github)  # (2)!
+access_token = auth.token
+
+user_github = github.with_auth(
+    OAuthTokenAuthStrategy("<client_id>", "<client_secret>", token=access_token)
+)  # (3)!
+
+# now you can act as the user
+resp = await user_github.rest.users.async_get_authenticated()
+user: PublicUser | PrivateUser = resp.parsed_data
+
+# you can get the user name and id now
+username = user.login
+user_id = user.id
+```
+
+1. If you just want to use the user token once, simply switch to OAuth Web Auth Strategy.
+2. exchange the user token manually and store it in a database.
+3. restore the user token from database
+
+If you are developing a GitHub APP with user-to-server token expiration:
+
+```python
+from githubkit.versions.latest.models import PublicUser, PrivateUser
+from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy
+
+github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
+
+# redirect user to github oauth page and get the code from callback
+
+user_github = github.with_auth(github.auth.as_web_user("<code>"))  # (1)!
+
+# === or ===
+auth: OAuthTokenAuthStrategy = await github.auth.as_web_user(
+    "<code>"
+).async_exchange_token(github)  # (2)!
+refresh_token = auth.refresh_token
+
+auth = OAuthTokenAuthStrategy(
+    "<client_id>", "<client_secret>", refresh_token=refresh_token
+)  # (3)!
+await auth.async_refresh(github)  # (4)!
+refresh_token = auth.refresh_token
+
+user_github = github.with_auth(auth)
+
+# now you can act as the user
+resp = await user_github.rest.users.async_get_authenticated()
+user: PublicUser | PrivateUser = resp.parsed_data
+
+# you can get the user name and id now
+username = user.login
+user_id = user.id
+```
+
+1. If you just want to use the user token once, simply switch to OAuth Web Auth Strategy.
+2. exchange the user token manually and store it in a database.
+3. restore the user refresh token from database and generate a new token.
+4. refresh the token manually and store the new one. otherwise, the token will be refreshed automatically when you make a request.
diff --git a/docs/usage/authentication.md b/docs/usage/authentication.md
new file mode 100644
index 000000000..0bbbd1c8c
--- /dev/null
+++ b/docs/usage/authentication.md
@@ -0,0 +1,339 @@
+# Authentication
+
+githubkit supports multiple authentication strategies to access the GitHub API. You can also switch between them easily when doing auth flow.
+
+## Without Authentication
+
+If you want to access GitHub API without authentication or make a temporary test, you can use `UnauthAuthStrategy`:
+
+```python
+from githubkit import GitHub, UnauthAuthStrategy
+
+# simply omit the auth parameter
+github = GitHub()
+# or, use UnauthAuthStrategy
+github = GitHub(UnauthAuthStrategy())
+```
+
+## Token Authentication
+
+You can authenticate to GitHub using a [Personal Access Token (PAT)](https://github.com/settings/personal-access-tokens/new). Replace `<your_token_here>` with your token:
+
+```python
+from githubkit import GitHub, TokenAuthStrategy
+
+# simply pass the token as a string
+github = GitHub("<your_token_here>")
+# or, use TokenAuthStrategy
+github = GitHub(TokenAuthStrategy("<your_token_here>"))
+```
+
+## GitHub APP Authentication
+
+If you are developing a GitHub App, you can authenticate using the GitHub App's private key and app ID. See [GitHub Docs - Authenticating as a GitHub App](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app) for more information.
+
+If you do not want to use OAuth features, you can omit the client ID and client secret. Replace `<app_id>`, `<private_key>`, `<optional_client_id>`, and `<optional_client_secret>` with your app's credentials:
+
+```python
+from githubkit import GitHub, AppAuthStrategy
+
+github = GitHub(
+    AppAuthStrategy(
+        "<app_id>", "<private_key>", "<optional_client_id>", "<optional_client_secret>"
+    )
+)
+```
+
+GitHub currently supports authentication using a GitHub App's private key and client ID. So, you could provide either the app ID or the client ID.
+
+```python
+from githubkit import GitHub, AppAuthStrategy
+
+github = GitHub(
+    AppAuthStrategy(
+        None, "<private_key>", "<client_id>", "<optional_client_secret>"
+    )
+)
+```
+
+## GitHub APP Installation Authentication
+
+GitHub Ap installation allows the app to access resources owned by that installation, as long as the app was granted the necessary repository access and permissions. API requests made by an app installation are attributed to the app. For more information, see [GitHub Docs - Authenticating as an installation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation).
+
+```python
+from githubkit import GitHub, AppInstallationAuthStrategy
+
+github = GitHub(
+    AppInstallationAuthStrategy(
+        "<app_id>", "<private_key>", installation_id, "<optional_client_id>", "<optional_client_secret>",
+    )
+)
+```
+
+App installation authentication also supports using the client ID instead of the app ID:
+
+```python
+from githubkit import GitHub, AppInstallationAuthStrategy
+
+github = GitHub(
+    AppInstallationAuthStrategy(
+        None, "<private_key>", installation_id, "<client_id>", "<optional_client_secret>",
+    )
+)
+```
+
+## OAuth APP Authentication
+
+If you are developing an OAuth App, you can authenticate using the client ID and client secret. See [GitHub Docs - Authenticating with an OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authenticating-to-the-rest-api-with-an-oauth-app) for more information.
+
+```python
+from githubkit import GitHub, OAuthAppAuthStrategy
+
+github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
+```
+
+## OAuth User Token Authentication
+
+OAuth App (GitHub App) allows you to act on behalf of a user after the user authorized your app. You can authenticate using the user token. This auth strategy is usefull when you stored the user token in a database.
+
+If you are developing an OAuth App or a GitHub App without user-to-server token expiration, you can authenticate using the user token directly:
+
+```python
+from githubkit import GitHub, OAuthTokenAuthStrategy
+
+github = GitHub(
+    OAuthTokenAuthStrategy(
+        "<client_id>", "<client_secret>", token="<access_token>"
+    )
+)
+```
+
+If you are developing a GitHub App with user-to-server token expiration, you need to provide the refresh token to keep the token up-to-date. In this case, you can omit the access token, but if you provide the access token, the access token expiration time is required:
+
+```python
+from datetime import datetime
+
+from githubkit import GitHub, OAuthTokenAuthStrategy
+
+github = GitHub(
+    OAuthTokenAuthStrategy(
+        "<client_id>",
+        "<client_secret>",
+        token="<access_token>",  # optional
+        expire_time=datetime(),  # optional
+        refresh_token="<refresh_token>",
+        refresh_token_expire_time=datetime(),  # optional
+    )
+)
+```
+
+If you want to refresh the access token immediately, you can normally call the `refresh` method:
+
+```python
+# sync
+auth = github.auth.refresh(github)
+# async
+auth = await github.auth.async_refresh(github)
+```
+
+This will update the access token and refresh token inplace. You can now store the new refresh token in `github.auth` to your database.
+
+## OAuth Web Flow Authentication
+
+GitHub OAuth web flow allows you to exchange the user access token with the web flow code. githubkit has built-in support for OAuth web flow token exchanging. See [GitHub Docs - Using the web application flow](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-web-application-flow-to-generate-a-user-access-token) for more information.
+
+```python
+from githubkit import GitHub, OAuthWebAuthStrategy
+
+github = GitHub(
+    OAuthWebAuthStrategy(
+        "<client_id_here>", "<client_secret_here>", "<web_flow_exchange_code_here>"
+    )
+)
+```
+
+Note that this auth strategy is only for one-time use. You need to store the user access token and refresh token in your database for future use:
+
+```python
+from githubkit import GitHub, OAuthWebAuthStrategy, OAuthTokenAuthStrategy
+
+github = GitHub(
+    OAuthWebAuthStrategy(
+        "<client_id_here>", "<client_secret_here>", "<web_flow_exchange_code_here>"
+    )
+)
+
+# sync
+auth: OAuthTokenAuthStrategy = github.auth.exchange_token(github)
+# async
+auth: OAuthTokenAuthStrategy = await github.auth.async_exchange_token(github)
+
+access_token = auth.token
+refresh_token = auth.refresh_token
+```
+
+See [Switch between AuthStrategy](#switch-between-authstrategy) for more detail about oauth flow.
+
+## OAuth Device Flow Authentication
+
+OAuth device flow allows you to authenticate as a user without a web browser (e.g., cli tools, desktop apps). githubkit has built-in support for OAuth device flow. See [GitHub Docs - Using the device flow](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-device-flow-to-generate-a-user-access-token) for more information.
+
+Before you start the device flow, you need to create a callback function to display the user code to the user. The callback function will be called when the user code is generated, and githubkit will poll the server until the user successfully authenticated or code expired.
+
+```python
+from githubkit import GitHub, OAuthDeviceAuthStrategy
+
+# sync/async func for displaying user code to user
+# the data dict is the generation response from the github server.
+# see the link above for more fields in the response.
+def callback(data: dict):
+  print(data["user_code"])
+
+github = GitHub(
+    OAuthDeviceAuthStrategy(
+        "<client_id_here>", callback
+    )
+)
+```
+
+Note that this auth strategy is only for one-time use too. You need to store the user access token and refresh token in your database for future use:
+
+```python
+from githubkit import GitHub, OAuthDeviceAuthStrategy, OAuthTokenAuthStrategy
+
+github = GitHub(
+    OAuthDeviceAuthStrategy(
+        "<client_id_here>", callback
+    )
+)
+
+# sync
+auth: OAuthTokenAuthStrategy = github.auth.exchange_token(github)
+# async
+auth: OAuthTokenAuthStrategy = await github.auth.async_exchange_token(github)
+
+access_token = auth.token
+refresh_token = auth.refresh_token
+```
+
+## GitHub Action Authentication
+
+githubkit provides a built-in auth strategy for GitHub Actions. You can use the `ActionAuthStrategy` to automatically authenticate to the GitHub API.
+
+```python
+from githubkit import GitHub, ActionAuthStrategy
+
+github = GitHub(ActionAuthStrategy())
+```
+
+and add input or env to the action step:
+
+```yaml
+- name: Some step use githubkit
+  with:
+    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+- name: Some step use githubkit
+  env:
+    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+```
+
+## Switch between AuthStrategy
+
+You can change the auth strategy and get a new client simplely using `with_auth`.
+
+Switch from GitHub App to a specific installation:
+
+```python
+from githubkit import GitHub, AppAuthStrategy
+
+github = GitHub(AppAuthStrategy("<app_id>", "<private_key>"))
+installation_github = github.with_auth(
+    github.auth.as_installation(installation_id)
+)
+```
+
+Switch from GitHub App to an OAuth App:
+
+```python
+from githubkit import GitHub, AppAuthStrategy, OAuthAppAuthStrategy
+
+github = GitHub(
+    AppAuthStrategy(
+        "<app_id>", "<private_key>", "<client_id>", "<client_secret>"
+    )
+)
+
+oauth_github = github.with_auth(github.auth.as_oauth_app())
+```
+
+Switch from OAuth App to a web user authorization (OAuth Web Flow):
+
+```python
+from githubkit import GitHub, OAuthAppAuthStrategy
+
+github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
+user_github = github.with_auth(github.auth.as_web_user("<code>"))
+
+# now you can act as the user
+resp = user_github.rest.users.get_authenticated()
+user = resp.parsed_data
+
+# you can get the user token after you maked a request as user
+user_token = user_github.auth.token
+user_token_expire_time = user_github.auth.expire_time
+refresh_token = user_github.auth.refresh_token
+refresh_token_expire_time = user_github.auth.refresh_token_expire_time
+```
+
+you can also get the user token directly without making a request (Switch from `OAuthWebAuthStrategy` to `OAuthTokenAuthStrategy`):
+
+```python
+from githubkit import GitHub, OAuthAppAuthStrategy, OAuthTokenAuthStrategy
+
+github = GitHub(OAuthAppAuthStrategy("<client_id>", "<client_secret>"))
+
+auth: OAuthTokenAuthStrategy = github.auth.as_web_user("<code>").exchange_token(github)
+# or asynchronously
+auth: OAuthTokenAuthStrategy = await github.auth.as_web_user("<code>").async_exchange_token(github)
+
+user_token = auth.token
+user_token_expire_time = auth.expire_time
+refresh_token = auth.refresh_token
+refresh_token_expire_time = auth.refresh_token_expire_time
+
+user_github = github.with_auth(auth)
+```
+
+Switch from `OAuthDeviceAuthStrategy` to `OAuthTokenAuthStrategy`:
+
+```python
+from githubkit import GitHub, OAuthDeviceAuthStrategy
+
+def callback(data: dict):
+    print(data["user_code"])
+
+user_github = GitHub(OAuthDeviceAuthStrategy("<client_id>", callback))
+
+# now you can act as the user
+resp = user_github.rest.users.get_authenticated()
+user = resp.parsed_data
+
+# you can get the user token after you maked a request as user
+user_token = user_github.auth.token
+user_token_expire_time = user_github.auth.expire_time
+refresh_token = user_github.auth.refresh_token
+refresh_token_expire_time = user_github.auth.refresh_token_expire_time
+
+# you can also exchange the token directly without making a request
+auth: OAuthTokenAuthStrategy = github.auth.exchange_token(github)
+# or asynchronously
+auth: OAuthTokenAuthStrategy = await github.auth.async_exchange_token(github)
+
+user_token = auth.token
+user_token_expire_time = auth.expire_time
+refresh_token = auth.refresh_token
+refresh_token_expire_time = auth.refresh_token_expire_time
+
+user_github = github.with_auth(auth)
+```
diff --git a/docs/usage/auto-retry.md b/docs/usage/auto-retry.md
new file mode 100644
index 000000000..9f4db4761
--- /dev/null
+++ b/docs/usage/auto-retry.md
@@ -0,0 +1,89 @@
+# Auto Retry
+
+By default, githubkit will retry the request when specific exception encountered. When rate limit exceeded, githubkit will retry **once** after GitHub suggested waiting time. When server error encountered (http status >= 500), githubkit will retry **max three times**.
+
+## Disable Auto Retry
+
+You can disable this feature by set the `auto_retry` option to `False`:
+
+```python
+github = GitHub(
+    ...
+    auto_retry=False
+)
+```
+
+## Customize Retry Decision
+
+You can also customize the retry decision by passing a callable. The callable should accept two arguments: the exception raised `exc` and the current retry count `retry_count`. The callable should return a `RetryOption` object. `RetryOption` is a named tuple with two fields: `do_retry` and `retry_after`. If `do_retry` is `True`, the request will be retried after `retry_after` time. Otherwise, the exception will be raised.
+
+```python hl_lines="6-9 13"
+from datetime import timedelta
+
+from githubkit.retry import RetryOption
+from githubkit.exception import GitHubException
+
+def retry_decision_func(exc: GitHubException, retry_count: int) -> RetryOption:
+    if retry_count < 1:
+        return RetryOption(True, timedelta(seconds=60))
+    return RetryOption(False)
+
+github = GitHub(
+    ...
+    auto_retry=retry_decision_func
+)
+```
+
+## Builtin Retry Decision
+
+githubkit also provides some builtin retry decision function.
+
+### Rate Limit Exceeded
+
+```python
+from githubkit.retry import RETRY_RATE_LIMIT, RetryRateLimit
+
+github = GitHub(
+  ...
+  auto_retry=RETRY_RATE_LIMIT  # (1)!
+)
+
+github = GitHub(
+  ...
+  auto_retry=RetryRateLimit(max_retry=1)  # (2)!
+)
+```
+
+1. Retry once when rate limit exceeded.
+2. Retry when rate limit exceeded with custom max retry count.
+
+### Server Error
+
+```python
+from githubkit.retry import RETRY_SERVER_ERROR, RetryServerError
+
+github = GitHub(
+   ...
+   auto_retry=RETRY_SERVER_ERROR  # (1)!
+)
+github = GitHub(
+   ...
+   auto_retry=RetryServerError(max_retry=1)  # (2)!
+)
+```
+
+1. Retry three times when server error encountered.
+2. Retry when server error encountered with custom max retry count.
+
+### Chain Retry Decision Functions
+
+You can chain multiple retry decision functions by using `RetryChainDecision`. The request will be retried if **any** of the decision functions return `True`. For example:
+
+```python
+from githubkit.retry import RETRY_RATE_LIMIT, RETRY_SERVER_ERROR, RetryChainDecision
+
+github = GitHub(
+   ...
+   auto_retry=RetryChainDecision(RETRY_RATE_LIMIT, RETRY_SERVER_ERROR)
+)
+```
diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md
new file mode 100644
index 000000000..11cc0eb2b
--- /dev/null
+++ b/docs/usage/configuration.md
@@ -0,0 +1,72 @@
+# Configuration
+
+githubkit is highly configurable, you can change the default config by passing config options to `GitHub`:
+
+```python
+from githubkit import GitHub
+
+github = GitHub(
+    base_url="https://api.github.com/",
+    accept_format=None,
+    previews=None,
+    user_agent="GitHubKit/Python",
+    follow_redirects=True,
+    timeout=None,
+    http_cache=True,
+    auto_retry=True,
+)
+```
+
+Or, you can pass the config object directly (not recommended):
+
+```python
+import httpx
+from githubkit import GitHub, Config
+from githubkit.retry import RETRY_DEFAULT
+
+config = Config(
+    base_url="https://api.github.com/",
+    accept="application/vnd.github+json",
+    user_agent="GitHubKit/Python",
+    follow_redirects=True,
+    timeout=httpx.Timeout(None),
+    http_cache=True,
+    auto_retry=RETRY_DEFAULT,
+)
+
+github = GitHub(config=config)
+```
+
+## Options
+
+### `base_url`
+
+The `base_url` option is used to set the base URL of the GitHub API. If you are using GitHub Enterprise Server, you need to include the `/api/v3/` path in the base URL.
+
+### `accept_format`, `previews`
+
+The `accept_format` and `previews` are used to set the default `Accept` header. By default, githubkit uses `application/vnd.github+json`. You can find more details in [GitHub API docs](https://docs.github.com/en/rest/overview/media-types).
+
+The `accept_format` option could be set to `PARAM+json`, such as `raw+json`.
+
+The `previews` option could be set to a list of preview features, such as `["starfox"]`. You can find the preview feature names in the GitHub API docs. Note that you do not need to include the `-preview` suffix in the preview feature name.
+
+### `user_agent`
+
+The `user_agent` option is used to set the `User-Agent` header. By default, githubkit uses `GitHubKit/Python`.
+
+### `follow_redirects`
+
+The `follow_redirects` option is used to enable or disable the HTTP redirect following feature. By default, githubkit follows the redirects.
+
+### `timeout`
+
+The `timeout` option is used to set the request timeout. You can pass a float, `None` or `httpx.Timeout` to this field. By default, the requests will never timeout. See [Timeout](https://www.python-httpx.org/advanced/timeouts/) for more information.
+
+### `http_cache`
+
+The `http_cache` option enables the http caching feature powered by [Hishel](https://hishel.com/) for HTTPX. GitHub API limits the number of requests that you can make within a specific amount of time. This feature is useful to reduce the number of requests to GitHub API and avoid hitting the rate limit.
+
+### `auto_retry`
+
+The `auto_retry` option enables request retrying when rate limit exceeded and server error encountered. See [Auto Retry](./auto-retry.md) for more infomation.
diff --git a/docs/usage/error-handling.md b/docs/usage/error-handling.md
new file mode 100644
index 000000000..ed8d54e0d
--- /dev/null
+++ b/docs/usage/error-handling.md
@@ -0,0 +1,52 @@
+# Error Handling
+
+githubkit will raise exceptions when it encounters an error. Here shows all the exceptions that githubkit may raise. The most common exception is `RequestFailed`, which is raised when the http response status code is not successful.
+
+```plaintext
+GitHubException
+├── AuthCredentialError
+├── AuthExpiredError
+├── RequestError
+├── RequestTimeout
+├── RequestFailed
+│   └── RateLimitExceeded
+│       ├── PrimaryRateLimitExceeded
+│       └── SecondaryRateLimitExceeded
+├── GraphQLError
+│   ├── GraphQLFailed
+│   └── GraphQLPaginationError
+│       ├── GraphQLMissingPageInfo
+│       └── GraphQLMissingCursorChange
+└── WebhookTypeNotFound
+```
+
+## Authentication Error
+
+githubkit will raise `AuthCredentialError` when you missing or provide invalid credentials. `AuthExpiredError` will be raised when the token or refresh token is expired.
+
+## Request Error
+
+All githubkit requests (including REST API and GraphQL API) may raise `RequestError` when the request failed. `RequestTimeout` will be raised when the request timeout. `RequestFailed` will be raised when the http response status code is not successful.
+
+```python
+try:
+    resp = github.rest.repos.get("owner", "repo")
+except RequestFailed as e:
+    print(e.response.status_code)
+except RequestTimeout as e:
+    print("Timeout")
+except RequestError as e:
+    print(f"Unknown Request Error: {e}")
+```
+
+Specially, `RateLimitExceeded` will be raised when githubkit detects the rate limit error message in the response. `PrimaryRateLimitExceeded` will be raised when the primary rate limit is exceeded. `SecondaryRateLimitExceeded` will be raised when the secondary rate limit is exceeded. See [GitHub Docs - Rate Limit](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api) for more information.
+
+## GraphQL Error
+
+githubkit may raise `GraphQLFailed` when the query failed. Note that this is not the same as `RequestFailed`.
+
+`GraphQLMissingPageInfo` will be raised when the page info missing in the response data. This usually happens when you miss the `pageInfo` field in the query. `GraphQLMissingCursorChange` will be raised when the cursor is not changed in the response data.
+
+## Webhook Error
+
+`WebhookTypeNotFound` will be raised when the webhook event name is not found. Please check the event name and the webhook version you are using or report an issue to githubkit.
diff --git a/docs/usage/graphql.md b/docs/usage/graphql.md
new file mode 100644
index 000000000..793614d6a
--- /dev/null
+++ b/docs/usage/graphql.md
@@ -0,0 +1,141 @@
+<!-- markdownlint-disable MD046 -->
+
+# Calling GraphQL API
+
+The GitHub GraphQL API offers flexibility and the ability to define precisely the data you want to fetch. See the [GitHub GraphQL API documentation](https://docs.github.com/en/graphql) for more information.
+
+## The Basics
+
+Before calling the GraphQL API, you could create your query from the [GitHub GraphQL Explorer](https://docs.github.com/en/graphql/overview/explorer). For example, to get current login user:
+
+```python
+query = """
+{
+  viewer {
+    login
+  }
+}
+"""
+```
+
+Then, you can call the GraphQL API with the query:
+
+=== "Sync"
+
+    ```python
+    data: Dict[str, Any] = github.graphql(query)
+    user_login: str = data["viewer"]["login"]
+    ```
+
+=== "Async"
+
+    ```python
+    data: Dict[str, Any] = await github.async_graphql(query)
+    user_login: str = data["viewer"]["login"]
+    ```
+
+Calling GraphQL API with variables:
+
+=== "Sync"
+
+    ```python
+    query = """
+    query ($owner: String!, $repo: String!) {
+      repository(owner: $owner, name: $repo) {
+        name
+      }
+    }
+    """
+
+    data: Dict[str, Any] = github.graphql(query, variables={"owner": "owner", "repo": "repo"})
+    repo_name: str = data["repository"]["name"]
+    ```
+
+=== "Async"
+
+    ```python
+    query = """
+    query ($owner: String!, $repo: String!) {
+      repository(owner: $owner, name: $repo) {
+        name
+      }
+    }
+    """
+
+    data: Dict[str, Any] = await github.async_graphql(query, variables={"owner": "owner", "repo": "repo"})
+    repo_name: str = data["repository"]["name"]
+    ```
+
+## GraphQL Pagination
+
+githubkit also provides a helper function to paginate the GraphQL API.
+
+First, You must accept a `cursor` parameter and return a `pageInfo` object in your query. For example:
+
+```graphql hl_lines="1 3 7-10"
+query ($owner: String!, $repo: String!, $cursor: String) {
+  repository(owner: $owner, name: $repo) {
+    issues(first: 10, after: $cursor) {
+      nodes {
+        number
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+}
+```
+
+The `pageInfo` object in your query must be one of the following types depending on the direction of the pagination:
+
+For forward pagination, use:
+
+```graphql
+pageInfo {
+  hasNextPage
+  endCursor
+}
+```
+
+For backward pagination, use:
+
+```graphql
+pageInfo {
+  hasPreviousPage
+  startCursor
+}
+```
+
+If you provide all 4 properties in a `pageInfo`, githubkit will default to **forward pagination**.
+
+Then, you can iterate over the paginated results by using the graphql `paginate` method:
+
+```python
+for result in github.graphql.paginate(
+    query, variables={"owner": "owner", "repo": "repo"}
+):
+    print(result)
+```
+
+Note that the `result` is a dict containing the list of nodes/edges for each page and the `pageInfo` object. You should iterate over the `nodes` or `edges` list to get the actual data. For example:
+
+```python
+for result in g.graphql.paginate(query, {"owner": "owner", "repo": "repo"}):
+    for issue in result["repository"]["issues"]["nodes"]:
+        print(issue)
+```
+
+You can also provide a initial cursor value to start pagination from a specific point:
+
+```python hl_lines="2"
+for result in github.graphql.paginate(
+    query, variables={"owner": "owner", "repo": "repo", "cursor": "initial_cursor"}
+):
+    print(result)
+```
+
+!!! tips
+
+    Nested pagination is not supported.
diff --git a/docs/usage/rest-api.md b/docs/usage/rest-api.md
new file mode 100644
index 000000000..c167990c2
--- /dev/null
+++ b/docs/usage/rest-api.md
@@ -0,0 +1,259 @@
+<!-- markdownlint-disable MD046 -->
+
+# Calling REST API
+
+GitHub's REST API enables you to build automation processes, integrate with GitHub and extend GitHub. See the [GitHub REST API documentation](https://docs.github.com/en/rest) for more information.
+
+> APIs are fully typed. Type hints in the following examples are just for reference only.
+
+## The Basics
+
+Calling REST API is simple with githubkit. You just need to create a `GitHub` instance with your authentication strategy and call the REST API methods in categories. For example, to get a repository:
+
+=== "Sync"
+
+    ```python hl_lines="5"
+    from githubkit import GitHub, Response
+    from githubkit.versions.latest.models import FullRepository
+
+    github = GitHub("<your_token_here>")
+    resp: Response[FullRepository] = github.rest.repos.get("owner", "repo")
+    repo: FullRepository = resp.parsed_data
+    ```
+
+=== "Async"
+
+    ```python hl_lines="5"
+    from githubkit import Response
+    from githubkit.versions.latest.models import FullRepository
+
+    github = GitHub("<your_token_here>")
+    resp: Response[FullRepository] = await github.rest.repos.async_get("owner", "repo")
+    repo: FullRepository = resp.parsed_data
+    ```
+
+If you are calling an API that requires request body parameters, you can pass the request body as a keyword argument:
+
+=== "Sync"
+
+    ```python hl_lines="5-10"
+    from githubkit import GitHub, Response
+    from githubkit.versions.latest.models import Issue
+
+    github = GitHub("<your_token_here>")
+    resp: Response[Issue] = github.rest.issues.create(
+        "owner",
+        "repo",
+        title="New Issue",
+        body="This is issue body",
+    )
+    issue: Issue = resp.parsed_data
+    ```
+
+=== "Async"
+
+    ```python hl_lines="5-10"
+    from githubkit import GitHub, Response
+    from githubkit.versions.latest.models import Issue
+
+    github = GitHub("<your_token_here>")
+    resp: Response[Issue] = await github.rest.issues.async_create(
+        "owner",
+        "repo",
+        title="New Issue",
+        body="This is issue body",
+    )
+    issue: Issue = resp.parsed_data
+    ```
+
+Or you can pass the request body as a dictionary:
+
+=== "Sync"
+
+    ```python hl_lines="8"
+    from githubkit import GitHub, Response
+    from githubkit.versions.latest.models import Issue
+
+    github = GitHub("<your_token_here>")
+    resp: Response[Issue] = github.rest.issues.create(
+        "owner",
+        "repo",
+        data={"title": "New Issue", "body": "This is issue body"},
+    )
+    issue: Issue = resp.parsed_data
+    ```
+
+=== "Async"
+
+    ```python hl_lines="8"
+    from githubkit import GitHub, Response
+    from githubkit.versions.latest.models import Issue
+
+    github = GitHub("<your_token_here>")
+    resp: Response[Issue] = await github.rest.issues.async_create(
+        "owner",
+        "repo",
+        data={"title": "New Issue", "body": "This is issue body"},
+    )
+    issue: Issue = resp.parsed_data
+    ```
+
+!!! danger
+
+    Note that you should hold a **strong reference** to the githubkit client instance. Otherwise, githubkit client will fail to call the request.
+    For example, you should not do this:
+
+    ```python
+    from githubkit import GitHub
+
+    def get_client() -> GitHub:
+        return GitHub()
+
+    # This will cause error
+    get_client().rest.repos.get("owner", "repo")
+
+    # This is ok
+    client = get_client()
+    client.rest.repos.get("owner", "repo")
+    ```
+
+## Reusing Client
+
+You can make multiple requests with the same client instance in one context:
+
+=== "Sync"
+
+    ```python hl_lines="4"
+    from githubkit import GitHub, Response
+    from githubkit.versions.latest.models import FullRepository
+
+    with GitHub("<your_token_here>") as github:
+        resp: Response[FullRepository] = github.rest.repos.get(owner="owner", repo="repo")
+        repo: FullRepository = resp.parsed_data
+    ```
+
+=== "Async"
+
+    ```python hl_lines="4"
+    from githubkit import GitHub, Response
+    from githubkit.versions.latest.models import FullRepository
+
+    async with GitHub("<your_token_here>") as github:
+        resp: Response[FullRepository] = await github.rest.repos.async_get(owner="owner", repo="repo")
+        repo: FullRepository = resp.parsed_data
+    ```
+
+## Data Validation
+
+As shown above, the response data is parsed and validated by accessing the `response.parsed_data` property. This ensures that the data type returned by the API is as expected and your code is safe to use it (with static type checking). But sometimes you may want to get the raw data returned by the API, such as when the schema is not correct. You can use the `response.text` property or `response.json()` method to get the raw data:
+
+```python hl_lines="5"
+from typing import Any, Dict
+from githubkit import Response
+
+resp: Response[FullRepository] = github.rest.repos.get("owner", "repo")
+repo: Dict[str, Any] = resp.json()
+```
+
+## REST API Versioning
+
+githubkit supports multiple versions of GitHub API, you can switch between versions as follows:
+
+```python
+github.rest("2022-11-28").repos.get("owner", "repo")
+```
+
+The code above uses the `2022-11-28` version of the GitHub API.
+
+Besides the REST API methods, the versioned models can also be imported from `githubkit.versions.<version>.models` module. For example:
+
+```python
+from githubkit.versions.v2022_11_28.models import FullRepository
+```
+
+Specially, the `latest` module is always linked to the latest version of GitHub API:
+
+```python
+from githubkit.versions.latest.models import FullRepository
+```
+
+!!! note
+
+    For backward compatibility, the `githubkit.rest` module is linked to the models of `latest` version by default.
+
+    ```python
+    from githubkit.rest import FullRepository
+    ```
+
+You can also get the latest version name of GitHub API and version-module mapping of GitHub API:
+
+```python
+from githubkit.versions import LATEST_VERSION, VERSIONS
+```
+
+Current supported versions are: (you can find it in the section `[[tool.codegen.descriptions]]` of the `pyproject.toml` file)
+
+- 2022-11-28 (latest)
+- ghec-2022-11-28
+
+## REST API Pagination
+
+When a response from the REST API would include many results, GitHub will paginate the results and return a subset of the results. In this case, some APIs provide `page` and `per_page` parameters to control the pagination. See [GitHub Docs - Using pagination in the REST API](https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api) for more information.
+
+githubkit provides a built-in pagination feature to handle this. You can use the `github.paginate` method to iterate over all the results:
+
+> Pagination typing is checked with Pylance ([Pyright](https://github.com/microsoft/pyright)).
+
+=== "Sync"
+
+    ```python hl_lines="3-5"
+    from githubkit.versions.latest.models import Issue
+
+    for issue in github.paginate(
+        github.rest.issues.list_for_repo, owner="owner", repo="repo", state="open"
+    ):
+        issue: Issue
+        print(issue.number)
+    ```
+
+=== "Async"
+
+    ```python hl_lines="3-5"
+    from githubkit.versions.latest.models import Issue
+
+    async for issue in github.paginate(
+        github.rest.issues.async_list_for_repo, owner="owner", repo="repo", state="open"
+    ):
+        issue: Issue
+        print(issue.number)
+    ```
+
+You can also provide a custom map function to handle complex pagination (such as when the API returns data in a nested field):
+
+=== "Sync"
+
+    ```python hl_lines="5"
+    from githubkit.versions.latest.models import Repository
+
+    for accessible_repo in github.paginate(
+        github.rest.apps.list_installation_repos_for_authenticated_user,
+        map_func=lambda r: r.parsed_data.repositories,
+        installation_id=1,
+    ):
+        accessible_repo: Repository
+        print(accessible_repo.full_name)
+    ```
+
+=== "Async"
+
+    ```python hl_lines="5"
+    from githubkit.versions.latest.models import Repository
+
+    async for accessible_repo in github.paginate(
+        github.rest.apps.async_list_installation_repos_for_authenticated_user,
+        map_func=lambda r: r.parsed_data.repositories,
+        installation_id=1,
+    ):
+        accessible_repo: Repository
+        print(accessible_repo.full_name)
+    ```
diff --git a/docs/usage/webhooks.md b/docs/usage/webhooks.md
new file mode 100644
index 000000000..808a334f7
--- /dev/null
+++ b/docs/usage/webhooks.md
@@ -0,0 +1,84 @@
+<!-- markdownlint-disable MD046 -->
+
+# Webhooks
+
+Webhooks provide a way for notifications to be delivered to an external web server whenever certain events occur on GitHub. See the [GitHub Webhooks Documentation](https://docs.github.com/en/webhooks) for more information.
+
+## Webhook Verification
+
+GitHub always sends a signature in the headers of the webhook request. You can verify the signature to ensure that the webhook request is from GitHub. See the [GitHub Docs - Validating webhook deliveries](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries) for more information.
+
+githubkit provides a `verify` function to help you verify the webhook payload. Note that you should provide the raw request body to ensure the integrity of the payload. For example:
+
+```python
+from githubkit.webhooks import verify
+
+valid: bool = verify(secret, request.body, request.headers["X-Hub-Signature-256"])
+```
+
+!!! note
+
+    The `verify` function is **time-constant**. This is to prevent timing attacks.
+
+!!! warning
+
+    The `verify` function also supports the `X-Hub-Signature` header. GitHub recommends that you use the `X-Hub-Signature-256` header, which uses the HMAC-SHA256 algorithm. The `X-Hub-Signature` header uses the HMAC-SHA1 algorithm and is only included for legacy purposes.
+
+You can also use the `sign` function to generate the signature for the payload. For example:
+
+```python
+from githubkit.webhooks import sign
+
+signature: str = sign(secret, payload, method="sha256")
+```
+
+## Webhook Event Parsing
+
+You can use githubkit to validate the webhook payload and convert it into a Pydantic model. githubkit provides a `parse` function and the function accepts the event name and the raw request body. For example:
+
+```python
+from githubkit.webhooks import parse
+
+event = parse(request.headers["X-GitHub-Event"], request.body)
+```
+
+(**NOT RECOMMENDED**) If you do not have the event name, you can use the `parse_without_name` function. This function will try to parse the payload with all supported event names. This will cost longer time, more memory and may return the wrong event type. For example:
+
+```python
+from githubkit.webhooks import parse_without_name
+
+event = parse_without_name(request.body)
+```
+
+!!! tip
+
+    The behavior of function `parse_without_name` is not the same between pydantic v1 and v2.
+
+    When using pydantic v1, the function will return the first valid event model (known as `left-to-right` mode).
+
+    When using pydantic v2, the function will return the highest scored valid event model (known as `smart` mode).
+
+    See: [Union Modes](https://docs.pydantic.dev/latest/concepts/unions/#union-modes).
+
+If you want to parse the payload as a dict, you can use the `parse_obj` function. For example:
+
+```python
+from githubkit.webhooks import parse_obj, parse_obj_without_name
+
+event = parse_obj(request.headers["X-GitHub-Event"], request.json())
+event = parse_obj_without_name(request.json())  # NOT RECOMMENDED
+```
+
+!!! note
+
+    The `parse` and `parse_obj` function supports type overload, if you provide static value for the `event_name` parameter, the return type will be inferred automatically.
+
+## Webhook Versioning
+
+Usually, you don't need to specify the version of the webhook payload. githubkit uses the latest version by default. If you want to use a specific version (including GHEC), you can switch between versions as follows:
+
+```python
+from githubkit import GitHub
+
+event = GitHub.webhooks("2022-11-28").parse(request.headers["X-GitHub-Event"], request.body)
+```
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 000000000..c96643942
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,144 @@
+# yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json
+
+site_name: GitHubKit Documentation
+site_url: https://yanyongyu.github.io/githubkit
+
+repo_name: yanyongyu/githubkit
+repo_url: https://github.com/yanyongyu/githubkit
+edit_uri: edit/master/docs/
+
+copyright: Copyright &copy; 2022 - 2024 GitHubKit Developers
+
+theme:
+  name: material
+  language: en
+
+  features:
+    - navigation.instant
+    - navigation.instant.progress
+    - navigation.tracking
+    - navigation.tabs
+    - navigation.tabs.sticky
+    # - navigation.sections
+    # - navigation.expand
+    - navigation.indexes
+    - navigation.top
+    - navigation.footer
+    - toc.follow
+    # - content.action.edit
+    # - content.action.view
+    - content.code.copy
+    - content.code.annotate
+    - content.tabs.link
+
+  icon:
+    edit: material/pencil
+    view: material/eye
+
+  palette:
+    - media: "(prefers-color-scheme)"
+      primary: black
+      toggle:
+        icon: material/brightness-auto
+        name: Switch to Light Mode
+
+    - scheme: default
+      media: "(prefers-color-scheme: light)"
+      primary: black
+      toggle:
+        icon: material/brightness-7
+        name: Switch to Dark Mode
+
+    - scheme: slate
+      media: "(prefers-color-scheme: dark)"
+      primary: black
+      toggle:
+        icon: material/brightness-4
+        name: Switch to System Preference
+
+extra_css:
+  - css/custom.css
+
+extra:
+  generator: false
+  social:
+    - icon: fontawesome/brands/github
+      link: https://github.com/yanyongyu/githubkit
+    - icon: fontawesome/brands/python
+      link: https://pypi.org/project/githubkit
+
+plugins:
+  - search
+  - git-revision-date-localized
+  - git-committers:
+      repository: yanyongyu/githubkit
+      branch: master
+
+markdown_extensions:
+  - admonition
+  - attr_list
+  - footnotes
+  - md_in_html
+  - toc:
+      permalink: true
+  - tables
+  - pymdownx.betterem:
+      smart_enable: all
+  - pymdownx.caret
+  - pymdownx.mark
+  - pymdownx.tilde
+  - pymdownx.details
+  - pymdownx.emoji:
+      emoji_index: !!python/name:material.extensions.emoji.twemoji
+      emoji_generator: !!python/name:material.extensions.emoji.to_svg
+  - pymdownx.highlight:
+      use_pygments: true
+  - pymdownx.superfences:
+      custom_fences:
+        - name: mermaid
+          class: mermaid
+          format: !!python/name:pymdownx.superfences.fence_code_format
+  - pymdownx.inlinehilite
+  - pymdownx.keys
+  - pymdownx.smartsymbols
+  - pymdownx.tabbed:
+      alternate_style: true
+      combine_header_slug: true
+      slugify: !!python/object/apply:pymdownx.slugs.slugify
+        kwds:
+          case: lower
+  - pymdownx.tasklist:
+      custom_checkbox: true
+  - pymdownx.magiclink:
+      repo_url_shortener: true
+      repo_url_shorthand: true
+      social_url_shorthand: true
+      user: yanyongyu
+      repo: githubkit
+
+nav:
+  - Home:
+      - Introduction: index.md
+      - Installation: installation.md
+      - Contributing: contributing.md
+  - Quick Start:
+      - quickstart/index.md
+      - Call API with PAT: quickstart/call-api-with-pat.md
+      - OAuth APP Web Flow: quickstart/oauth-web-flow.md
+      - OAuth APP Device Flow: quickstart/oauth-device-flow.md
+      - GitHub APP: quickstart/github-app.md
+  - Usage:
+      - Authentication: usage/authentication.md
+      - Configuration: usage/configuration.md
+      - REST API: usage/rest-api.md
+      - GraphQL: usage/graphql.md
+      - Auto Retry: usage/auto-retry.md
+      - Webhooks: usage/webhooks.md
+      - Unit Test: usage/unit-test.md
+      - Error Handling: usage/error-handling.md
+
+validation:
+  omitted_files: warn
+  absolute_links: warn
+  unrecognized_links: warn
+  anchors: warn
diff --git a/poetry.lock b/poetry.lock
index afb68ebbd..a2542e37a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -36,6 +36,23 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)",
 test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"]
 trio = ["trio (>=0.26.1)"]
 
+[[package]]
+name = "babel"
+version = "2.16.0"
+description = "Internationalization utilities"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"},
+    {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"},
+]
+
+[package.dependencies]
+pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
+
+[package.extras]
+dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
+
 [[package]]
 name = "certifi"
 version = "2024.8.30"
@@ -137,6 +154,119 @@ files = [
     {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
 ]
 
+[[package]]
+name = "charset-normalizer"
+version = "3.3.2"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+    {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+    {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
 [[package]]
 name = "colorama"
 version = "0.4.6"
@@ -355,6 +485,37 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.
 testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"]
 typing = ["typing-extensions (>=4.12.2)"]
 
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+description = "Copy your docs directly to the gh-pages branch."
+optional = false
+python-versions = "*"
+files = [
+    {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
+    {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
+]
+
+[package.dependencies]
+python-dateutil = ">=2.8.1"
+
+[package.extras]
+dev = ["flake8", "markdown", "twine", "wheel"]
+
+[[package]]
+name = "gitdb"
+version = "4.0.11"
+description = "Git Object Database"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"},
+    {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"},
+]
+
+[package.dependencies]
+smmap = ">=3.0.1,<6"
+
 [[package]]
 name = "githubkit-test"
 version = "0.1.0"
@@ -375,6 +536,24 @@ pytest-xdist = "^3.5.0"
 type = "directory"
 url = "envs/test"
 
+[[package]]
+name = "gitpython"
+version = "3.1.43"
+description = "GitPython is a Python library used to interact with Git repositories"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff"},
+    {file = "GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c"},
+]
+
+[package.dependencies]
+gitdb = ">=4.0.1,<5"
+
+[package.extras]
+doc = ["sphinx (==4.3.2)", "sphinx-autodoc-typehints", "sphinx-rtd-theme", "sphinxcontrib-applehelp (>=1.0.2,<=1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (>=2.0.0,<=2.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)"]
+test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"]
+
 [[package]]
 name = "h11"
 version = "0.14.0"
@@ -557,6 +736,24 @@ files = [
     {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
 ]
 
+[[package]]
+name = "markdown"
+version = "3.7"
+description = "Python implementation of John Gruber's Markdown."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"},
+    {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
+
+[package.extras]
+docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
+testing = ["coverage", "pyyaml"]
+
 [[package]]
 name = "markupsafe"
 version = "2.1.5"
@@ -626,6 +823,143 @@ files = [
     {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
 ]
 
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+description = "A deep merge function for 🐍."
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"},
+    {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"},
+]
+
+[[package]]
+name = "mkdocs"
+version = "1.6.1"
+description = "Project documentation with Markdown."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"},
+    {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"},
+]
+
+[package.dependencies]
+click = ">=7.0"
+colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
+ghp-import = ">=1.0"
+importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
+jinja2 = ">=2.11.1"
+markdown = ">=3.3.6"
+markupsafe = ">=2.0.1"
+mergedeep = ">=1.3.4"
+mkdocs-get-deps = ">=0.2.0"
+packaging = ">=20.5"
+pathspec = ">=0.11.1"
+pyyaml = ">=5.1"
+pyyaml-env-tag = ">=0.1"
+watchdog = ">=2.0"
+
+[package.extras]
+i18n = ["babel (>=2.9.0)"]
+min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"]
+
+[[package]]
+name = "mkdocs-get-deps"
+version = "0.2.0"
+description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"},
+    {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
+mergedeep = ">=1.3.4"
+platformdirs = ">=2.2.0"
+pyyaml = ">=5.1"
+
+[[package]]
+name = "mkdocs-git-committers-plugin-2"
+version = "2.4.0"
+description = "An MkDocs plugin to create a list of contributors on the page. The git-committers plugin will seed the template context with a list of GitHub or GitLab committers and other useful GIT info such as last modified date"
+optional = false
+python-versions = "<4,>=3.8"
+files = [
+    {file = "mkdocs_git_committers_plugin_2-2.4.0-py3-none-any.whl", hash = "sha256:4ca97c06e6b0f651db878afbdb1fec24c0eb841d9b8985161c2fb7837c87f0a3"},
+    {file = "mkdocs_git_committers_plugin_2-2.4.0.tar.gz", hash = "sha256:b4d21f64f9f135c8c1bbb353e374a61f48b195497e83ba695d96f3211e3d0518"},
+]
+
+[package.dependencies]
+gitpython = "*"
+mkdocs = ">=1.0.3"
+requests = "*"
+
+[[package]]
+name = "mkdocs-git-revision-date-localized-plugin"
+version = "1.2.9"
+description = "Mkdocs plugin that enables displaying the localized date of the last git modification of a markdown file."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs_git_revision_date_localized_plugin-1.2.9-py3-none-any.whl", hash = "sha256:dea5c8067c23df30275702a1708885500fadf0abfb595b60e698bffc79c7a423"},
+    {file = "mkdocs_git_revision_date_localized_plugin-1.2.9.tar.gz", hash = "sha256:df9a50873fba3a42ce9123885f8c53d589e90ef6c2443fe3280ef1e8d33c8f65"},
+]
+
+[package.dependencies]
+babel = ">=2.7.0"
+GitPython = "*"
+mkdocs = ">=1.0"
+pytz = "*"
+
+[package.extras]
+all = ["GitPython", "babel (>=2.7.0)", "click", "codecov", "mkdocs (>=1.0)", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkdocs-material", "mkdocs-static-i18n", "pytest", "pytest-cov", "pytz"]
+base = ["GitPython", "babel (>=2.7.0)", "mkdocs (>=1.0)", "pytz"]
+dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkdocs-material", "mkdocs-static-i18n", "pytest", "pytest-cov"]
+
+[[package]]
+name = "mkdocs-material"
+version = "9.5.39"
+description = "Documentation that simply works"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs_material-9.5.39-py3-none-any.whl", hash = "sha256:0f2f68c8db89523cb4a59705cd01b4acd62b2f71218ccb67e1e004e560410d2b"},
+    {file = "mkdocs_material-9.5.39.tar.gz", hash = "sha256:25faa06142afa38549d2b781d475a86fb61de93189f532b88e69bf11e5e5c3be"},
+]
+
+[package.dependencies]
+babel = ">=2.10,<3.0"
+colorama = ">=0.4,<1.0"
+jinja2 = ">=3.0,<4.0"
+markdown = ">=3.2,<4.0"
+mkdocs = ">=1.6,<2.0"
+mkdocs-material-extensions = ">=1.3,<2.0"
+paginate = ">=0.5,<1.0"
+pygments = ">=2.16,<3.0"
+pymdown-extensions = ">=10.2,<11.0"
+regex = ">=2022.4"
+requests = ">=2.26,<3.0"
+
+[package.extras]
+git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"]
+imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"]
+recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"]
+
+[[package]]
+name = "mkdocs-material-extensions"
+version = "1.3.1"
+description = "Extension pack for Python Markdown and MkDocs Material."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
+    {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
+]
+
 [[package]]
 name = "nodeenv"
 version = "1.9.1"
@@ -690,6 +1024,32 @@ files = [
     {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
 ]
 
+[[package]]
+name = "paginate"
+version = "0.5.7"
+description = "Divides large result sets into pages for easier browsing"
+optional = false
+python-versions = "*"
+files = [
+    {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"},
+    {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"},
+]
+
+[package.extras]
+dev = ["pytest", "tox"]
+lint = ["black"]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+    {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
 [[package]]
 name = "platformdirs"
 version = "4.3.6"
@@ -888,6 +1248,20 @@ files = [
 [package.dependencies]
 typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
 
+[[package]]
+name = "pygments"
+version = "2.18.0"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
+    {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
 [[package]]
 name = "pyjwt"
 version = "2.9.0"
@@ -908,6 +1282,24 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte
 docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
 tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
 
+[[package]]
+name = "pymdown-extensions"
+version = "10.11.1"
+description = "Extension pack for Python Markdown."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pymdown_extensions-10.11.1-py3-none-any.whl", hash = "sha256:a2b28f5786e041f19cb5bb30a1c2c853668a7099da8e3dd822a5ad05f2e855e3"},
+    {file = "pymdown_extensions-10.11.1.tar.gz", hash = "sha256:a8836e955851542fa2625d04d59fdf97125ca001377478ed5618e04f9183a59a"},
+]
+
+[package.dependencies]
+markdown = ">=3.6"
+pyyaml = "*"
+
+[package.extras]
+extra = ["pygments (>=2.12)"]
+
 [[package]]
 name = "pytest"
 version = "8.3.3"
@@ -968,6 +1360,31 @@ psutil = ["psutil (>=3.0)"]
 setproctitle = ["setproctitle"]
 testing = ["filelock"]
 
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+    {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+    {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pytz"
+version = "2024.2"
+description = "World timezone definitions, modern and historical"
+optional = false
+python-versions = "*"
+files = [
+    {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
+    {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
+]
+
 [[package]]
 name = "pyyaml"
 version = "6.0.2"
@@ -1030,6 +1447,144 @@ files = [
     {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
 ]
 
+[[package]]
+name = "pyyaml-env-tag"
+version = "0.1"
+description = "A custom YAML tag for referencing environment variables in YAML files. "
+optional = false
+python-versions = ">=3.6"
+files = [
+    {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
+    {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
+]
+
+[package.dependencies]
+pyyaml = "*"
+
+[[package]]
+name = "regex"
+version = "2024.9.11"
+description = "Alternative regular expression module, to replace re."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"},
+    {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"},
+    {file = "regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5"},
+    {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c"},
+    {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8"},
+    {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35"},
+    {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71"},
+    {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8"},
+    {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a"},
+    {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d"},
+    {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137"},
+    {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6"},
+    {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca"},
+    {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a"},
+    {file = "regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0"},
+    {file = "regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623"},
+    {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df"},
+    {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268"},
+    {file = "regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad"},
+    {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679"},
+    {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4"},
+    {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664"},
+    {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"},
+    {file = "regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199"},
+    {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4"},
+    {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd"},
+    {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f"},
+    {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96"},
+    {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1"},
+    {file = "regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9"},
+    {file = "regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf"},
+    {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7"},
+    {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231"},
+    {file = "regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d"},
+    {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64"},
+    {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42"},
+    {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766"},
+    {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a"},
+    {file = "regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9"},
+    {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d"},
+    {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822"},
+    {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0"},
+    {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a"},
+    {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a"},
+    {file = "regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776"},
+    {file = "regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009"},
+    {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784"},
+    {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36"},
+    {file = "regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92"},
+    {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86"},
+    {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85"},
+    {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963"},
+    {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6"},
+    {file = "regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802"},
+    {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29"},
+    {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8"},
+    {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84"},
+    {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554"},
+    {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8"},
+    {file = "regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8"},
+    {file = "regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f"},
+    {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4"},
+    {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e"},
+    {file = "regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60"},
+    {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b"},
+    {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366"},
+    {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8"},
+    {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb"},
+    {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4"},
+    {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca"},
+    {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb"},
+    {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168"},
+    {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e"},
+    {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c"},
+    {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd"},
+    {file = "regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771"},
+    {file = "regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508"},
+    {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066"},
+    {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62"},
+    {file = "regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16"},
+    {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3"},
+    {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199"},
+    {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8"},
+    {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca"},
+    {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9"},
+    {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a"},
+    {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39"},
+    {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba"},
+    {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664"},
+    {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89"},
+    {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35"},
+    {file = "regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142"},
+    {file = "regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919"},
+    {file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"},
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+    {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
+]
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<4"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<3"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
+
 [[package]]
 name = "ruff"
 version = "0.6.8"
@@ -1057,6 +1612,28 @@ files = [
     {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"},
 ]
 
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
+[[package]]
+name = "smmap"
+version = "5.0.1"
+description = "A pure Python implementation of a sliding window memory map manager"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"},
+    {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"},
+]
+
 [[package]]
 name = "sniffio"
 version = "1.3.1"
@@ -1090,6 +1667,23 @@ files = [
     {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
 ]
 
+[[package]]
+name = "urllib3"
+version = "2.2.3"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
+    {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+h2 = ["h2 (>=4,<5)"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
 [[package]]
 name = "virtualenv"
 version = "20.26.6"
@@ -1110,6 +1704,53 @@ platformdirs = ">=3.9.1,<5"
 docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
 test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
 
+[[package]]
+name = "watchdog"
+version = "4.0.2"
+description = "Filesystem events monitoring"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22"},
+    {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1"},
+    {file = "watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503"},
+    {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930"},
+    {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b"},
+    {file = "watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef"},
+    {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a"},
+    {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29"},
+    {file = "watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a"},
+    {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b"},
+    {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d"},
+    {file = "watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7"},
+    {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040"},
+    {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7"},
+    {file = "watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4"},
+    {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9"},
+    {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578"},
+    {file = "watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b"},
+    {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa"},
+    {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3"},
+    {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508"},
+    {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee"},
+    {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1"},
+    {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e"},
+    {file = "watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83"},
+    {file = "watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c"},
+    {file = "watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a"},
+    {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73"},
+    {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc"},
+    {file = "watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757"},
+    {file = "watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8"},
+    {file = "watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19"},
+    {file = "watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b"},
+    {file = "watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c"},
+    {file = "watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270"},
+]
+
+[package.extras]
+watchmedo = ["PyYAML (>=3.10)"]
+
 [[package]]
 name = "wcwidth"
 version = "0.2.13"
@@ -1150,4 +1791,4 @@ jwt = ["PyJWT"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "17dc054635d323b134ed0dffc27e1a762e7ef90a56c0a09781a0b950d8a8e396"
+content-hash = "b4342b7b1b1c2fcae2ce7fa61ce0f466e145a7e57b28e1c9c0e0f8be8ec6f94f"
diff --git a/pyproject.toml b/pyproject.toml
index 86e0ae4f8..af5dd9da4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,14 @@ tomli = { version = "^2.0.1", python = "<3.11" }
 [tool.poetry.group.test.dependencies]
 githubkit-test = { path = "./envs/test", develop = false }
 
+
+[tool.poetry.group.docs.dependencies]
+pygments = "^2.18.0"
+mkdocs-material = "^9.5.39"
+pymdown-extensions = "^10.11.1"
+mkdocs-git-committers-plugin-2 = "^2.4.0"
+mkdocs-git-revision-date-localized-plugin = "^1.2.9"
+
 [tool.poetry.extras]
 jwt = ["PyJWT"]
 auth-app = ["PyJWT"]
diff --git a/scripts/setup-envs.sh b/scripts/setup-envs.sh
index 10d754164..0eead73da 100755
--- a/scripts/setup-envs.sh
+++ b/scripts/setup-envs.sh
@@ -7,7 +7,7 @@ poetry config virtualenvs.in-project true
 
 # setup dev environment
 echo "Setting up dev environment"
-poetry install --all-extras && poetry run pre-commit install
+poetry install --all-extras --with docs && poetry run pre-commit install
 
 # setup pydantic v2 test environment
 for env in $(find ./envs/ -maxdepth 1 -mindepth 1 -type d -not -name test); do


Test patch:
diff --git a/docs/usage/unit-test.md b/docs/usage/unit-test.md
new file mode 100644
index 000000000..a52063810
--- /dev/null
+++ b/docs/usage/unit-test.md
@@ -0,0 +1,108 @@
+# Unit Test
+
+If you are using githubkit in your business logic, you may want to mock the github API in your unit tests. You can custom the response by mocking the `request`/`arequest` method of the `GitHub` class. Here is an example of how to mock githubkit's API calls:
+
+=== "Sync"
+
+    ```python
+    import json
+    from pathlib import Path
+    from typing import Any, Type, Union
+
+    import httpx
+    import pytest
+
+    from githubkit import GitHub
+    from githubkit.utils import UNSET
+    from githubkit.response import Response
+    from githubkit.typing import URLTypes, UnsetType
+    from githubkit.versions.latest.models import FullRepository
+
+    FAKE_RESPONSE = json.loads(Path("fake_response.json").read_text())
+
+    def target_sync_func() -> FullRepository:  # (1)!
+        github = GitHub("xxxxx")
+        resp = github.rest.repos.get("owner", "repo")
+        return resp.parsed_data
+
+    def mock_request(
+        g: GitHub,
+        method: str,
+        url: URLTypes,
+        *,
+        response_model: Union[Type[Any], UnsetType] = UNSET,
+        **kwargs: Any,  # (2)!
+    ) -> Response[Any]:
+        if method == "GET" and url == "/repos/owner/repo":  # (3)!
+            return Response[T](
+                httpx.Response(status_code=200, json=FAKE_RESPONSE),
+                Any if response_model is UNSET else response_model,
+            )
+        raise RuntimeError(f"Unexpected request: {method} {url}")
+
+    # Test the target function
+    def test_sync_mock():
+        with pytest.MonkeyPatch.context() as m:
+            # Patch the request method with the mock
+            m.setattr(GitHub, "request", mock_request)
+
+            repo = target_sync_func()
+            assert isinstance(repo, FullRepository)
+    ```
+
+    1. Example function you want to test, which calls the GitHub API.
+    2. other request parameters including headers, json, etc.
+    3. When the request is made, return a fake response
+
+=== "Async"
+
+    ```python
+    import json
+    from pathlib import Path
+    from typing import Any, Type, Union
+
+    import httpx
+    import pytest
+
+    from githubkit import GitHub
+    from githubkit.utils import UNSET
+    from githubkit.response import Response
+    from githubkit.typing import URLTypes, UnsetType
+    from githubkit.versions.latest.models import FullRepository
+
+    FAKE_RESPONSE = json.loads(Path("fake_response.json").read_text())
+
+    async def target_async_func() -> FullRepository:  # (1)!
+        async with GitHub("xxxxx") as github:
+            resp = await github.rest.repos.get("owner", "repo")
+            return resp.parsed_data
+
+    async def mock_arequest(
+        g: GitHub,
+        method: str,
+        url: URLTypes,
+        *,
+        response_model: Union[Type[Any], UnsetType] = UNSET,
+        **kwargs: Any,  # (2)!
+    ) -> Response[Any]:
+        if method == "GET" and url == "/repos/owner/repo":  # (3)!
+            return Response[T](
+                httpx.Response(status_code=200, json=FAKE_RESPONSE),
+                Any if response_model is UNSET else response_model,
+            )
+        raise RuntimeError(f"Unexpected request: {method} {url}")
+
+    # Test the target function
+    @pytest.mark.anyio
+    async def test_async_mock():
+        with pytest.MonkeyPatch.context() as m:
+            # Patch the request method with the mock
+            m.setattr(GitHub, "arequest", mock_arequest)
+
+            repo = await target_async_func()
+            assert isinstance(repo, FullRepository)
+    ```
+
+    1. Example function you want to test, which calls the GitHub API.
+    2. other request parameters including headers, json, etc.
+    3. When the request is made, return a fake response