Code Monkey home page Code Monkey logo

ossapi's Introduction

ossapi (documentation) PyPI version

ossapi is the definitive python wrapper for the osu! api. ossapi has complete coverage of api v2 and api v1, and provides both sync (Ossapi) and async (OssapiAsync) versions for api v2.

If you need support or would like to contribute, feel free to ask in the #ossapi channel of the circleguard discord.

Installation

To install:

pip install ossapi
# or, if you want to use OssapiAsync:
pip install ossapi[async]

To upgrade:

pip install -U ossapi

To get started, read the docs: https://tybug.github.io/ossapi/.

Quickstart

The docs have an in depth quickstart, but here's a super short version for api v2:

from ossapi import Ossapi

# create a new client at https://osu.ppy.sh/home/account/edit#oauth
api = Ossapi(client_id, client_secret)

# see docs for full list of endpoints
print(api.user("tybug").username)
print(api.user(12092800, mode="osu").username)
print(api.beatmap(221777).id)

Async

ossapi provides an async variant, OssapiAsync, which has an identical interface to Ossapi:

import asyncio
from ossapi import OssapiAsync

api = OssapiAsync(client_id, client_secret)

async def main():
    await api.user("tybug")

asyncio.run(main())

Read more about OssapiAsync on the docs.

Other domains

You can use ossapi to interact with the api of other deployments of the osu website, such as https://dev.ppy.sh.

from ossapi import Ossapi

api = Ossapi(client_id, client_secret, domain="dev")
# get the dev server pp leaderboards
ranking = api.ranking("osu", "performance").ranking
# pearline06, as of 2023
print(ranking[0].user.username)

Read more about domains on the docs.

Endpoints

All endpoints for api v2.

API v1 Usage

You can get your api v1 key at https://osu.ppy.sh/home/account/edit#legacy-api.

Basic usage:

from ossapi import OssapiV1

api = OssapiV1("key")
print(api.get_beatmaps(user=53378)[0].submit_date)
print(api.get_match(69063884).games[0].game_id)
print(api.get_scores(221777)[0].username)
print(len(api.get_replay(beatmap_id=221777, user=6974470)))
print(api.get_user(12092800).playcount)
print(api.get_user_best(12092800)[0].pp)
print(api.get_user_recent(12092800)[0].beatmap_id)

ossapi's People

Contributors

tybug avatar fluffetyfluff avatar invisiblesymbol avatar magnus-cosmos avatar techno-coder avatar samuelhklumpers avatar aticie avatar itsmestro avatar alchzh avatar maskeddd avatar ssz7-ch2 avatar zium1337 avatar

Stargazers

Ignacior avatar Anko_6go avatar Noelle avatar 柚子 avatar  avatar Tim Harris avatar  avatar  avatar Scyhei avatar Saltssaumure avatar william avatar Moonlight Dorkreamer avatar TRANCE avatar  avatar Casey Primozic avatar Biggest Aot Glazer avatar FrZ avatar JokelBaf avatar Raikashi avatar Wishy8839 avatar Michael Deng avatar Agatem avatar  avatar Akanecco_2323 avatar Carrotkun avatar Eunho Lee avatar Michel Barros avatar  avatar  avatar Raj Pandya avatar  avatar chrystom (flido) avatar Daniel Yang avatar Up in All avatar Yoon Hyungseok avatar  avatar snipe avatar FireFall avatar Kotoki avatar  avatar 咕谷酱 avatar Jamie Tong avatar 840 avatar  avatar Oluwamayowa Esan avatar sea_mods avatar  avatar Darien avatar Denis Kornilov avatar Jason avatar Vitaly Tkachov avatar Ari van Houten avatar  avatar Deltara avatar  avatar blobnom avatar Bemmy avatar jam avatar Owen avatar Andrés Orozco  avatar Кирилл avatar Luz avatar Aditya Gupta avatar  avatar ZANXEY avatar  avatar  avatar Ghoul avatar  avatar  avatar Roan Song avatar Yoru avatar Carter avatar Watanabe Rui avatar  avatar Ben Brady avatar flido avatar praeludium avatar

Watchers

James Cloos avatar Kostas Georgiou avatar Trafis avatar Vitaly Tkachov avatar Raikashi avatar

ossapi's Issues

Some beatmap()/beatmap_attributes() variables are returned as int instead of float

When beatmap() or beatmap_attributes() is called, whenever a Beatmap or BeatmapDifficultyAttributes instance has integral values for some of its variables (CS, AR, OD, HP, BPM) (e.g. CS 4/AR 9/OD 8/HP 5/180 BPM), those values get returned as int instead of float, despite the annotation saying that the return type should always be float.

I believe this might also apply to all of the _difficulty variables for beatmap_attributes() whenever the actual value is 0, such as flashlight_difficulty when FL isn't actually passed as a mod.

TypeError: Ossapi.__init__() got an unexpected keyword argument 'headers'

I'm trying to get information from a beatmap using my api key, but this error keeps happening, what I'm doing wrong?

import ossapi

Replace "YOUR_API_KEY" with your own API key

api_key = "My api key"

Create a dictionary of headers to set the user agent

headers = {"User-Agent": "MyAwesomeApp/1.0"}

Create an Ossapi object using your API key and headers

api = ossapi.Ossapi(api_key, headers=headers)

Replace "BEATMAP_ID" with the ID of the beatmap you want to get information for

beatmap_id = "1514385"

Get the beatmap information using the get_beatmaps method

The method returns a list of beatmap objects, so we access the first item in the list

beatmap = api.get_beatmaps(beatmap_id=beatmap_id)[0]

Open a text file for writing

filename = f"{beatmap_id}.txt" # Use the beatmap ID as the filename
with open(filename, "w") as f:

# Write the beatmap information to the file
f.write(f"Title: {beatmap.title}\n")
f.write(f"Artist: {beatmap.artist}\n")
f.write(f"Creator: {beatmap.creator}\n")
f.write(f"Difficulty name: {beatmap.version}\n")
f.write(f"Stars: {beatmap.difficulty_rating}\n")
f.write(f"Circle size: {beatmap.circle_size}\n")
f.write(f"Overall difficulty: {beatmap.overall_difficulty}\n")
f.write(f"Approach rate: {beatmap.approach_rate}\n")
f.write(f"HP drain: {beatmap.hp_drain}\n")
f.write(f"Max combo: {beatmap.max_combo}\n")
f.write(f"Length (in seconds): {beatmap.total_length}\n")
f.write(f"BPM: {beatmap.bpm}\n")
f.write(f"Source: {beatmap.source}\n")
f.write(f"Tags: {beatmap.tags}\n")
f.write(f"Download link: {beatmap.download_link}\n")

print(f"Beatmap information written to {filename}")

rework saving credentials

saving credentials locally in a pickle file in the ossapi install location is a pretty bad idea:

  • it makes handling multiple scopes difficult, since you cannot maintain multiple distinct credentials simultaneously (one for one set of scopes and one for another)
  • it prevents the user from handling their own authentication, including where to save it, when to save it, if it should be saved, when and how to delete it, etc.

It does however have the advantage of being easy to use. Consumers don't need to worry about an auth file or class, they just instantiate and go.

This needs a lot of thought. The more I think about it the more difficult this is going to be to implement properly while still not being an overbearing burden on the majority of users, who are using ossapi for a personal script. What follows is my best current proposal, but I may decide this is an equally terrible direction when I try implementing it.

I think we should continue to save auth files in ossapi's install site (by default, but optionally let consumers set a token_directory to save to), but instead of hardcoding the filename use the hash of the client_id, client_secret, flow, and scopes (I don't think we want to hash redirect_uri) as the filename to avoid conflicts. This would allow us to keep as many different ossapiv2 instances / authentications as we wanted, so we could have two differently scoped ossapiv2's running around. We still get the full refresh benefit for both since when we instantiate OssapiV2, it hashes the relevant arguments, checks for a file matching that hash, and uses the contents if so.

Something to keep in mind is that this is still limited to only a single user. If one user authenticates and then another tries authenticating with the same parameters, I'm pretty sure it would just use the previous user's credentials. Multiple users isn't something we support right now anyway, but we should in the future. I think a system like the above makes it easier to do so if we expose the ability to optionally associate an id of some sort to each file. We could add a auth_file_key parameter to OssapiV2 for instance, which looks for a file by that name (or adds that key to the previous parameters and hashes them, if we wanted to make these unique up to the other arguments as well) first, then authenticates and saves to a file by that name (or again, the hash of the key and the other params) if it can't find the file. This would allow lookups via an arbitrary key, which could eg be the web session id of that user, or the user's actual id if that's known ahead of time.

progress towards implementing api v2 endpoints

This tracks our progress towards implementing all of the api v2 endpoints; see docs at https://osu.ppy.sh/docs/index.html. The plan is to implement every endpoint except for lazer-only ones. Even undocumented endpoints should be implemented.

PRs implementing any of these endpoints are welcome. It should be relatively easy to do so - add the relevant models if we don't have them already, and add a new method in OssapiV2 that hits that endpoint. Look at the existing models and methods in OssapiV2 for examples on how to implement an endpoint.

Beatmaps

Beatmapsets

Beatmapset Discussions

Changelog

Chat

Comments

Forum

Friends

Home

Matches

Multiplayer

News

Oauth Tokens

Ranking

Rooms

Seasonal Backgrounds

Scores

Users

Wiki

Handle responses >= 500

Either raise an error or retry. Could also move all our RequestException handling code from core to ossapi

authorization doesn't work on Amazon Linux

I am currently trying to host my bot on an aws server and when the osu website is trying to send the data to localhost. It is telling me that the connection is getting refused. I made sure that both osu and my program have the same localhost ports.

I did look a bit into it and it should be possible to create a webserver on localhost so I dont understand why it doesnt work.

censorship on point
(censorship on point)

to view the website I am using elinks.

For python I am using 3.10

I installed these packages into my instance via yum:
xz-devel
lzma
zlib-devel
gcc
openssl-devel
bzip2-devel
libffi-devel
make
elinks

and I installed these libraries into my python:
discord
pycord
ossapi
pillow
sqlalchemy
pymysql

using client credentials grant for longer than 24 hours likely errors

A long living (>24hr) instance of OssapiV2 which was instantiated with the client credentials grant is very likely to error on the next request since our token has expired by then.

We'll need to try and catch the token expired error on every request and refresh the token if we do. Which was the proper solution to #21 anyway but I was hoping we could get away with something simpler. It appears we cannot.

(I say "very likely" because I have not had a script running long enough to run into this yet.)

Token directory should be created

        self.token_directory = (
            Path(token_directory) if token_directory else Path(__file__).parent
        )

Currently if a non-existing token directory is given, this error occurs:

  File "C:\Users\efeha\PycharmProjects\osu-map-downloader\main.py", line 199, in <module>
    main()
  File "C:\Users\efeha\PycharmProjects\osu-map-downloader\main.py", line 94, in main
    api = OssapiV2(client_id=credentials.get("client_id"), client_secret=credentials.get("client_secret"),
  File "C:\Users\efeha\PycharmProjects\osu-map-downloader\venv\lib\site-packages\ossapi\ossapiv2.py", line 294, in __init__
    self.session = self.authenticate()
  File "C:\Users\efeha\PycharmProjects\osu-map-downloader\venv\lib\site-packages\ossapi\ossapiv2.py", line 359, in authenticate
    return self._new_client_grant(self.client_id, self.client_secret)
  File "C:\Users\efeha\PycharmProjects\osu-map-downloader\venv\lib\site-packages\ossapi\ossapiv2.py", line 374, in _new_client_grant
    self._save_token(token)
  File "C:\Users\efeha\PycharmProjects\osu-map-downloader\venv\lib\site-packages\ossapi\ossapiv2.py", line 431, in _save_token
    with open(self.token_file, "wb+") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'abc\\bc7d75f22d1ebc0626b3e9cb595027d58236e659de6cb7801af5533269678079.pickle'

If no token directory is given, this error occurs when an executable is created using PyInstaller because Path(__file__).parent points to a non-existing folder.

user scores returns empty list for recent

Running the ossapi trying to get the most recent score sometimes returns an empty list. Does the api get the most recent game in 18 hours or does it get the most recent game in general? Because I am unsure if it doesn't work because I didn't play in the last 24 hours or if it just doesn't work all the time for some other reason

help | Getting a user's recent score

So I'm continuing from that other issue, How do I get a user's recent score?
image
Something like this, I can't seem to find anything on the documentation.

Invalid GameMode 123

Im trying to make automatic clip bot, that scrapes user replays, downloads .osr, and does other shit.

I tried to use ossapi to scrape and download a replay, but as soon as Im trying to download a replay,
it throws ValueError: 123 is not a valid GameMode.

Code:

from ossapi import OssapiV2
api = OssapiV2("app id", "app key", "redirect url")
import requests
import base64

def get_replay(userid):
    """This function gets the latest replay.
    
Returns None if no new recent replays are made.
Else it returns the replay."""

    # gets the 30th top play pp
    wtp = api.user_scores(userid, "best", limit=30)[-1].pp
    # gets the last 10 scores
    scores = score = api.user_scores(userid, "recent", limit=10)
    # opens the replays.txt, so we dont process replays that have been already rendered and shit
    rendered = open("replays.txt").read().splitlines()
    for cn in range(0, 60):
        score = scores[cn] # grabs score
        pp = score.pp # gets pp
        if pp == None: # loved maps return None PP, so i change it to 0 if its none to make it work normally
            pp = 0
        score_id = score.id # score id
        print(wtp, pp, score.pp, cn, score.id) # debugging, ignore

        # checks if the score pp is more than 30th top play, and isnt rendered
        if pp >= wtp and score_id not in rendered:
            api.download_score(mode="osu", score_id=score.id)
            print("found")
            break

# call
get_replay(11917029)

Token directory should not default to anything in the installed package

Currently you have:

Path(token_directory) if token_directory else Path(__file__).parent
which is a bad default e.g. here:

with open(self.token_file, "wb+") as f:

because this is against python packaging best practices: one should not expect the location of the installed package to be writeable or even or even an actual directory/file on the filesystem. Leading to errors like this on some systems:

OSError: [Errno 30] Read-only file system: '/home/confus/devel/project/venv/lib/python3.11/site-packages/ossapi/e1e06....708f7.pickle'

Even on systems where that works, it's putting user-private data into a potentially world-readable location without the user's knowledge.

Ossapi should probably use something like platformdirs or importlib.resources, which uses sane defaults and the XDG spec to determine this default location. It should probably be checked if that sort of thing is a problem anywhere else in the code.

Pointed out to me by @KreconyMakaron

Pagination causes issues with 'page=200'

When trying to pull results from page 200 (users 9951-10000) the whole thing kinda falls apart. Works for all other pages i tested including 199 but fails on 200.

from ossapi import *
api = OssapiV2("xxxx", "xxxxxxxxxxxxxxxxxxxxx")
cursor = Cursor(page=200)
r = api.ranking("osu", RankingType.SCORE, cursor=cursor)
print(r.ranking[-1].user.username)
print(api.search(query=r.ranking[-1].user.username).user.data[0].id)
print(api.user(api.search(query=r.ranking[-1].user.username).user.data[0].id).statistics.ranked_score)
Traceback (most recent call last):
  File "C:/Users/xbdev/Documents/osu test.py", line 4, in <module>
    r = api.ranking("osu", RankingType.SCORE, cursor=cursor)
  File "C:\Users\xbdev\AppData\Local\Programs\Python\Python39\lib\site-packages\ossapi\ossapiv2.py", line 84, in wrapper
    return function(*args, **kwargs)
  File "C:\Users\xbdev\AppData\Local\Programs\Python\Python39\lib\site-packages\ossapi\ossapiv2.py", line 691, in ranking
    return self._get(Rankings, f"/rankings/{mode.value}/{type_.value}",
  File "C:\Users\xbdev\AppData\Local\Programs\Python\Python39\lib\site-packages\ossapi\ossapiv2.py", line 214, in _get
    return self._instantiate_type(type_, json_)
  File "C:\Users\xbdev\AppData\Local\Programs\Python\Python39\lib\site-packages\ossapi\ossapiv2.py", line 385, in _instantiate_type
    return self._resolve_annotations(value)
  File "C:\Users\xbdev\AppData\Local\Programs\Python\Python39\lib\site-packages\ossapi\ossapiv2.py", line 306, in _resolve_annotations
    value = self._instantiate_type(type_, value, obj, attr_name=attr)
  File "C:\Users\xbdev\AppData\Local\Programs\Python\Python39\lib\site-packages\ossapi\ossapiv2.py", line 380, in _instantiate_type
    value = self._instantiate(type_, value)
  File "C:\Users\xbdev\AppData\Local\Programs\Python\Python39\lib\site-packages\ossapi\ossapiv2.py", line 408, in _instantiate
    for key in list(kwargs):
TypeError: 'NoneType' object is not iterable

best_id with null values

best_id is denoted as a non-optional field, on the osu!web Documentation page. But not having it as an optional field causes the _resolve_annotations function to raise an exception as it always expects best_id to be a valid integer.

>>> api = ossapi.Osapi = ossapi.OssapiV2(client_id, client_secret, redirect_uri)
>>> api.log.setLevel('DEBUG')
>>>
>>> api.user_scores(8945180, ossapi.ScoreType.RECENT)[0]
made request to https://osu.ppy.sh/api/v2/users/8945180/scores/recent
received json: https://pastebin.com/raw/FVFGAYnY
instantiating type <class 'ossapi.models.Score'>
ignoring unexpected parameter `passed` from api response for type <class 'ossapi.models.Score'>
resolving [...]
resolved  [...]
Score(...)
>>>
>>>
>>> api.user_scores(14644448, ossapi.ScoreType.RECENT)
made request to https://osu.ppy.sh/api/v2/users/14644448/scores/recent
received json: https://pastebin.com/raw/aPEH44RQ
instantiating type <class 'ossapi.models.Score'>
ignoring unexpected parameter `passed` from api response for type <class 'ossapi.models.Score'>
resolving annotations for type <class 'ossapi.models.Score'>
resolving attribute id
resolving attribute best_id
<class 'int'> None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\nwxnk\AppData\Roaming\Python\Python39\site-packages\ossapi\ossapiv2.py", line 83, in wrapper
    return function(*args, **kwargs)
  File "C:\Users\nwxnk\AppData\Roaming\Python\Python39\site-packages\ossapi\ossapiv2.py", line 682, in user_scores
    return self._get(List[Score], f"/users/{user_id}/scores/{type_.value}",
  File "C:\Users\nwxnk\AppData\Roaming\Python\Python39\site-packages\ossapi\ossapiv2.py", line 213, in _get
    return self._instantiate_type(type_, json_)
  File "C:\Users\nwxnk\AppData\Roaming\Python\Python39\site-packages\ossapi\ossapiv2.py", line 370, in _instantiate_type
    entry = self._resolve_annotations(entry)
  File "C:\Users\nwxnk\AppData\Roaming\Python\Python39\site-packages\ossapi\ossapiv2.py", line 305, in _resolve_annotations
    value = self._instantiate_type(type_, value, obj, attr_name=attr)
  File "C:\Users\nwxnk\AppData\Roaming\Python\Python39\site-packages\ossapi\ossapiv2.py", line 335, in _instantiate_type
    raise TypeError(f"expected type {type_} for value {value}, got "
TypeError: expected type <class 'int'> for value None, got type <class 'NoneType'> (for attribute: best_id)

TypeError thrown for Beatmap objects lacking a BPM

Beatmap objects that have no BPM cause a TypeError; for example:

api.beatmap(beatmap_id=2279323)
throws:
TypeError: expected type <class 'float'> for value None, got type <class 'NoneType'> (for attribute: bpm)

beatmap page for id 2279323:
image

implement token refreshing for client credentials grant

Sorry for the bother if this is an easy fix or I'm doing something wrong, I'm a bit new to this..

I have:

from ossapi import *
api = OssapiV2(client_id=8114 client_secret='xxxxxxxxxxxx')

endpoints were all working perfectly but after a few days it looks like the session expires:

Traceback (most recent call last):
  File "C:\Users\Mathew\Desktop\Mathew\Programs\mouseonlyrecords\main.py", line 69, in <module>
    user_scores = api.user_scores(user_id=p[0], type_="best", limit=51, offset=0)
  File "C:\Users\Mathew\Desktop\Mathew\Programs\mouseonlyrecords\venv\lib\site-packages\ossapi\ossapiv2.py", line 83, in wrapper
    return function(*args, **kwargs)
  File "C:\Users\Mathew\Desktop\Mathew\Programs\mouseonlyrecords\venv\lib\site-packages\ossapi\ossapiv2.py", line 681, in user_scores
    return self._get(List[Score], f"/users/{user_id}/scores/{type_.value}",
  File "C:\Users\Mathew\Desktop\Mathew\Programs\mouseonlyrecords\venv\lib\site-packages\ossapi\ossapiv2.py", line 203, in _get
    r = self.session.get(f"{self.BASE_URL}{url}", params=params)
  File "C:\Users\Mathew\Desktop\Mathew\Programs\mouseonlyrecords\venv\lib\site-packages\requests\sessions.py", line 555, in get
    return self.request('GET', url, **kwargs)
  File "C:\Users\Mathew\Desktop\Mathew\Programs\mouseonlyrecords\venv\lib\site-packages\requests_oauthlib\oauth2_session.py", line 477, in request
    url, headers, data = self._client.add_token(
  File "C:\Users\Mathew\Desktop\Mathew\Programs\mouseonlyrecords\venv\lib\site-packages\oauthlib\oauth2\rfc6749\clients\base.py", line 198, in add_token
    raise TokenExpiredError()
oauthlib.oauth2.rfc6749.errors.TokenExpiredError: (token_expired)

ValueError: '' is not a valid GameMode

When I use the following code snippet, a ValueError exception is thrown:

api = OssapiV2(Config.OSU["client_id"], Config.OSU["client_secret"])

# Both don't work
api.user(username, "", UserLookupKey.USERNAME)
api.user(username, None, UserLookupKey.USERNAME)

But the documentation specifies otherwise

Rare TypeError when calling `api.events`

Example:

api.events() # If I don't call this first in this case, it seems to just return the old Event model without additional event-specific information
cursor_string = "eyJldmVudF9pZCI6ODI4MDA1NDgyfQ"
sort_str = "id_asc"
events = api.events(cursor_string=cursor_string, sort=sort_str)

returns

- TypeError: 'NoneType' object is not iterable

Did some debugging and I believe it's something to do with supporter gift events - I think this is the specific event that triggers the error in this case:

UserSupportGiftEvent(created_at=datetime.datetime(2024, 1, 8, 4, 54, 11, tzinfo=datetime.timezone.utc), 
                     createdAt=datetime.datetime(2024, 1, 8, 4, 54, 11, tzinfo=datetime.timezone.utc), 
                     id=828005503, 
                     type=<EventType.USER_SUPPORT_GIFT: 'userSupportGift'>, 
                     beatmap=None)

Error on scores with xK mod (mania)

The short names in int_to_mod (mod.py) should probably be xK instead of Kx

Example:

score = api.score(score_id=468410699, mode="mania")

returns

- ValueError: Invalid mod string (no matching mod found for 4K)

(also, bumping the Event -> _Event change in #66)

'UserSupportAgainEvent' Attribute Error when using limit greater than default

Saw this happen with api.user_recent_activity.

for recent_activity in api.user_recent_activity(user_id=12345, limit=4):
    print(recent_activity.scoreRank)

This works well but whenever I change the limit to be greater than 5, it always gives this error

AttributeError: 'UserSupportAgainEvent' object has no attribute 'scoreRank'

If I check the type of recent_activity whenever the limit is greater than 4, it returns RankEvent or RankLostEvent, so what is the UserSupportAgainEvent?

don't raise on null model attributes by default

in a similar vein to ignoring extra parameters to models, we likely also don't want to raise when a model field marked as optional is given a null value, so as to keep forward-compatibility. The osu api sometimes decides that parameters should be optional after all and we don't want to break all old ossapi versions when they do so.

This should only have an effect when strict is False.

an asynchronous version of the v2 API wrapper

it would be quite useful for website backends, with logins and discord bots, that also want to authenticate users in a way
https://async-oauthlib.readthedocs.io/en/latest/ there's an async oauth module, that seems to fit the features needed for it
and for requests themself, aiohttp would be nice
it could be named something like AsyncOssapiV2 which would be used the same as the synchronous one, except that all endpoints would need to be awaited

high level abstractions / conveniences

api v2 is complex enough that it probably makes sense for us to provide some abstractions to help consumers. Things I've thought of (but not attempted implementing, and so might not work as well as I envisioned):

allow User / UserCompact as arguments (done in b068521)

allow User or UserCompact to be passed to methods that normally require a user_id or username. Internally we'd just take user.id and supply that.

PaginatedModel

instead of exposing cursors to the user directly, return models which handle pagination for us:

class PaginatedModel(Model):
    def __init__(self, cursor, *args, **kwargs):
        self.cursor = cursor
        super().__init__(*args, **kwargs)

    def next(self):
        """
        Retrieves and returns the next page of results. Raises `NoPagesLeftException` if 
        there are no more pages to paginate. You can also check ``next_page_exists`` 
        to see if you can paginate. (TODO: better name than ``next_page_exists``)
        """
        ...

    def next_page_exists(self):
        return bool(self.cursor)

will probably need some restructuring to allow models to make requests (should they also store a reference to their calling api object?)

Then users will never need to know about cursors, unless they want to retrieve a specific attribute, which will still be possible through model.cursor.whatever.

Tough question: should cursors be removed as parameters to functions? The argument for yes is that ideally consumers would not be keeping references to raw cursors in case they need to retrieve the next page, but rather keeping a reference to the entire PaginatedModel.

However this isn't ideal if they hold that object for a long time because then they're also holding all of its response content in memory instead of just the cursor, and responses can be fairly big. Perhaps we should still expose the cursor parameter as an advanced option, but discourage its use unless you know what you're doing? I think that's the right path forward.

Expansion of compact models (done in c32abe4)

Some endpoints only return a *Compact model, but you might want the full model. eg api.search only returns UserCompact, not User. We should have something like the following:

class Expandable(ABC):
    @abstractmethod
    def expand(self):
        pass

class UserCompact(Model, Expandable)
class BeatmapCompact(Model, Expandable)
class BeatmapsetCompact(Model, Expandable)

Then:

user = api.search(query="tybug").user.data[0]
# before
print(api.user(user.id).statistics.ranked_score)
# after
print(user.expand().statistics.ranked_score)

standardize attribute, method, argument, etc names

every forward-facing name needs a pass to make sure they're consistent and make sense, as the names given by the api do not always do either. For example:

  • Search.{user, wiki_page} should probably be Search.{users, wiki_pages}
  • we might want to rename User.id to User.user_id, and similarly for Beatmap and Beatmapset (I'm not entirely sold on this, but a straight id attribute isn't very intuitive to me. Perhaps others feel differently)

AttributeError: 'Search' object has no attribute 'user'

Installed ossapi the way it was mentioned in the readme and got the required credentials.

from ossapi import *

api = OssapiV2(foo, bar, uri)
print(api.search(query="peppy").user.data[0].profile_colour)

Outputs an AttributeError that search does not have an attribute with the name 'user'

Score IDs and endpoint has been changed

There seems to be new score IDs for all scores which makes retrieving a score using the older endpoint not work with the new score IDs. The endpoint has changed from 'scores/{mode}/{score_id}' to 'scores/{score_id}' although the older endpoint still works with the older score IDs.

GameMode.CTB Returning STD

When trying to gather a user's top plays in CTB, I receive their top std plays instead.

top = api.user_scores(userid, "best", GameMode.CTB, limit=50)
print(top)

> returns std scores

I have tried using GameMode.CTB, GameMode.CATCH, mode="ctb" and mode="Catch" (oddly enough I get 'catch' is not a valid GameMode when all lowercase), all returning the same thing. Changing the gamemode works for all other modes except ctb.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.