nifty-anilist 0.1.7
Utilities for the Anilist GraphQL API.
Requires Python: >=3.12

# Nifty Anilist Tools

## Overview

This is a simple utility library for interfacing with the [Anilist GraphQL API](https://docs.anilist.co/).
It provides useful tools like an Anilist client to make validated requests with query builder objects and handles authentication for you.

### Using the Library

This library is available on [PyPi](https://pypi.org/project/nifty-anilist/).

To use this library, you will need to have the variables shown in [.env.example](./.env.example) in environment variables or your local `.env` file.
Any blank variables need to be present and will throw an error if are missing.

## Features

### GraphQL Requests
The Anilist API is GraphQL-based and provides a [public schema](https://studio.apollographql.com/sandbox/schema/reference). This library uses an [Ariadne](https://ariadnegraphql.org/client/intro) code-generated GraphQL client to make GraphQL requests to Anilist.

To make requests to Anilist, create an `AnilistClient` and use the `anilist_request()` function. Example:
```py
from nifty_anilist import AnilistClient 

def my_function(my_query):
    client = AnilistClient()
    async with client:
        data = await client.anilist_request(my_query)

# OR

from nifty_anilist import AnilistClient

def my_function(my_query):
    async with AnilistClient() as client:
        data = await client.anilist_request(my_query)
```

#### Prebuilt Queries

This library provides a few helpful pre-built queries to Anilist for common operations. These are very customizable, but also come with sensible default values so your code can be very short.
Additionally, they return type-checked objects so you have a guaranteed structure and don't have to go through string->Any dictionaries for dozen of lines.
These queries are imported from `nifty_anilist.prebuilt`.

These queries can be used as follows:
```py
from nifty_anilist.client.custom_fields import MediaListStatus, MediaType
from nifty_anilist.prebuilt import get_user_media_list, UserMediaListFilters

async def get_user_completed_anime_list(username: str):
    async with AnilistClient() as client:
        filters = UserMediaListFilters(
            type=MediaType.ANIME, status_in=[MediaListStatus.COMPLETED]
        )
        anime_list = await get_user_media_list(
            client, user_name=username, list_filters=filters
        )
```

These functions all require a client as input and will sometimes have function-specific mandatory inputs. In most cases, leaving the `list_filters` parameter empty will use all defaults.

Below is a list of currently available pre-built queries.

- **[Media](./nifty_anilist/prebuilt/media.py)**: These are queries for `Query.Page.media` in the Anilist API.
    - `get_media_list()`: Get a list of media (anime or manga).
- **[Media List](./nifty_anilist/prebuilt/media_list.py)**: These are queries for `Query.Page.mediaList` in the Anilist API.
    - `get_user_media_list()`: Get a user's list of media (anime or manga).
    - `get_my_media_list()` Get the current global user's list of media (anime or manga).

#### Custom Queries

One of the benefits of Ariadne is that queries (and manipulations, etc.) are made with objects instead of raw GraphQL strings, so you get a sort of schema pre-validation for your requests. These objects are also generated by Ariadne and are available from the `nifty_anilist.client.custom_XXX` files. Feel free to reference the [Anilist schema](https://studio.apollographql.com/sandbox/schema/reference) and use the [playground](https://studio.apollographql.com/sandbox/explorer?endpoint=https://graphql.anilist.co) before writing these queries in code.

The following is a simple example for building such a query for getting a user's avatar:
```py
from nifty_anilist.client.custom_queries import Query
from nifty_anilist.client.custom_fields import UserFields, UserAvatarFields

...

query = Query.user(name="username").fields(
    UserFields.avatar().fields(
        UserAvatarFields.large
    )
)

response = await client.anilist_request(query)

avatar_url = response["User"]["avatar"]["large"]
print(f"Avatar URL: {avatar_url}")
```

There is also a helper function for making paginated requests: `paginated_anilist_request()`. This function allows you to skip setting up the `Page` field in your query and merging all the pages by looping through them.
Instead, you can just provide the (sub)field you actually want to query. Example of getting all of a user's completed anime:
```py
query = Query.media_list(
    user_name="username",
    type=MediaType.ANIME,
    status_in=[MediaListStatus.COMPLETED],
    sort=[MediaListSort.SCORE_DESC, MediaListSort.MEDIA_ID],
).fields(
    MediaListFields.score(format=ScoreFormat.POINT_100),
    MediaListFields.media().fields(
        MediaFields.title().fields(
            MediaTitleFields.native(),
            MediaTitleFields.english(),
        )
    )
)

response = await client.paginated_anilist_request(query)

for anime in response:
    print(f"{anime["media"]["title"]["native"]} ({anime["media"]["title"]["english"]}): {anime["score"]}/100")
```

The client takes an optional user ID to make requests for. If not provided, the client will try to use global user. You can also turn off authentication by setting `use_auth` to `False`.
These settings should not be changed for an existing client; you should make a new one if you need to make requests under a different user's credentials.

### Anilist Auth

#### Token Storage
This library will store Anilist auth token(s) locally in one of two ways (customizable with the `TOKEN_SAVING_METHOD` environment variable):
1. Using your system's keyring (kept "permanently"): `KEYRING`
2. In-memory (lost when you restart your program): `IN_MEMORY`

#### Using Auth
In order to get an auth token for the first time, you have 2 options:
1. Use the `sign_in_if_no_global()` function from [auth.py](./nifty_anilist/auth.py). After the first time, these details will be stored locally and added to your Anilist requests automatically.
2. If you already have an auth token from some other process, you can "sign in" with the `sign_in_with_token()` function from [auth.py](./nifty_anilist/auth.py). By default this function will set the user of this token as the global one, but this can be disabled with the `set_global` parameter.

**Note:** The sign-in function will open an instance of the browser defined by `ANILIST_AUTH_CODE_BROWSER` to the Anilist login page, from which your auth code will be automatically extracted. Chrome is the default.

There are two ways that auth headers can be added to your requests:
1. Using a global user ID stored in the `.env` file: The ID is stored as `ANILIST_CURRENT_USER`. This is the user ID that will be used to retrieve the token from the storage method(s) above. When making requests with the client's `anilist_request()`, you can ignore the optional `user_id` parameter to use this approach. Example:
```py
async def do_something():
    query = # some query
    async with AnilistClient() as client:
        return await client.anilist_request(query)

if __name__ == "__main__":
    sign_in_if_no_global()
    data = asyncio.run(do_something())
    print(data)
```
2. Manually passing in a user ID to the `anilist_request()` with the `user_id` parameter will try to get that user's token from local storage and use it instead of the global one. Example:
```py
async def do_something(user_id: str):
    query = # some query
    async with AnilistClient(user_id=user_id) as client:
        return await client.anilist_request(query)

if __name__ == "__main__":
    my_user_id = "12345"
    sign_in_with_token(my_user_id)
    data = asyncio.run(do_something(user_id=my_user_id))
    print(data)
```

**Note:** You can also choose to not use auth on requests with the `use_auth` parameter (default is `True`). In this case you don't need to sign it at all. Example:
```py
async def do_something():
    query = # some query
    async with AnilistClient(use_auth=False) as client:
        return await client.anilist_request(query)
```