From 873e820e78c3197a09a33617bc204b0337f4051a Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 5 Feb 2019 21:25:36 +0100 Subject: [PATCH] Create package --- LICENSE.txt | 21 ++++++ README.md | 32 +++++++++ requirements.txt | 3 + setup.py | 24 +++++++ steamleaderboards/__init__.py | 130 ++++++++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 steamleaderboards/__init__.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..bcffd5d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Stefano Pigozzi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b58cf0c --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# `steamleaderboards` + +A package designed to help developers access the leaderboards of various Steam games. + +It was created with the Isaac Daily Run scoreboards in mind, but it can be used for other games as well. + +## Usage + +To use `steamleaderboards`, first create a `LeaderboardGroup` for the desired game. + +```python +import steamleaderboards as sl +lbgroup = sl.LeaderboardGroup(STEAM_APP_ID) +``` + +Once you have created the `LeaderboardGroup`, you can retrieve the desired leaderboards by using the `LeaderboardGroup.get` method. +You can specify the name, the display name or the id of the leaderboard to retrieve. + +```python +leaderboard_a = lbgroup.get(name=LEADERBOARD_NAME) +leaderboard_b = lbgroup.get(lbid=LEADERBOARD_ID) +leaderboard_c = lbgroup.get(display_name=LEADERBOARD_DISPLAY_NAME) +``` + +When you have the `Leaderboard` object, you can find all the entries in the `Leaderboard.entries` field, or you can search for a specific one through the `Leaderboard.find_entry` method. + +```python +all_scores = leaderboard_a.entries +my_score = leaderboard_a.find_entry(MY_STEAMID_1) +first_place_score = leaderboard_a.find_entry(rank=1) +last_place_score = leaderboard_a.find_entry(rank=-1) +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3d9f8c9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +lxml +requests +bs4 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5f4d071 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +import setuptools + +with open("README.md", "r") as f: + long_description = f.read() + +setuptools.setup( + name="steamleaderboards-steffo", + version="0.0.1", + author="Stefano Pigozzi", + author_email="ste.pigozzi@gmail.com", + description="A wrapper for the Steam Leaderboards", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/Steffo99/steamleaderboards", + packages=setuptools.find_packages(), + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.6", + "Topic :: Internet" + ] +) \ No newline at end of file diff --git a/steamleaderboards/__init__.py b/steamleaderboards/__init__.py new file mode 100644 index 0000000..db52275 --- /dev/null +++ b/steamleaderboards/__init__.py @@ -0,0 +1,130 @@ +import requests +from bs4 import BeautifulSoup +import typing + + +class LeaderboardGroup: + def __init__(self, app_id): + xml = requests.get(f"https://steamcommunity.com/stats/{app_id}/leaderboards/?xml=1") + _bs = BeautifulSoup(xml.content, features="lxml") + self.leaderboards = [] + for leaderboard in _bs.find_all("leaderboard"): + self.leaderboards.append(ProtoLeaderboard(leaderboard, app_id)) + + def __repr__(self): + return f"" + + def get(self, name=None, *, display_name=None) -> typing.Optional["Leaderboard"]: + """Get the full leaderboard with the specified parameter.""" + if bool(lbid) + bool(name) + bool(display_name) > 1: + raise ValueError("You can only find a leaderboard by 1 parameter.") + if lbid is not None: + if not isinstance(lbid, int): + raise ValueError("lbid must be an int") + for leaderboard in self.leaderboards: + if leaderboard.lbid == lbid: + return leaderboard.full() + elif name is not None: + if not isinstance(name, str): + raise ValueError("name must be a str") + for leaderboard in self.leaderboards: + if leaderboard.name == name: + return leaderboard.full() + elif display_name is not None: + if not isinstance(display_name, str): + raise ValueError("display_name must be a str") + for leaderboard in self.leaderboards: + if leaderboard.display_name == display_name: + return leaderboard.full() + return None + + +class ProtoLeaderboard: + """Information about a leaderboard retrieved through a leaderboard""" + def __init__(self, soup, app_id): + self.url = soup.url.text + self.lbid = int(soup.lbid.text) + self.name = soup.find("name").text + self.display_name = soup.display_name.text + self.entries = int(soup.entries.text) + self.sort_method = int(soup.sortmethod.text) + self.display_type = int(soup.displaytype.text) + self.app_id = app_id + + def full(self) -> "Leaderboard": + return Leaderboard(protoleaderboard=self) + + +class Leaderboard: + # noinspection PyMissingConstructor + def __init__(self, app_id=None, lbid=None, *, protoleaderboard=None): + if protoleaderboard: + self.url = protoleaderboard.url + self.lbid = protoleaderboard.lbid + self.name = protoleaderboard.name + self.display_name = protoleaderboard.display_name + self.entries = protoleaderboard.entries + self.sort_method = protoleaderboard.sort_method + self.display_type = protoleaderboard.display_type + self.app_id = protoleaderboard.app_id + elif app_id and lbid: + self.lbid = lbid + self.app_id = app_id + self.url = None + self.name = None + self.display_name = None + self.entries = None + self.sort_method = None + self.display_type = None + else: + raise ValueError("No app_id, lbid or protoleaderboard specified") + next_request_url = f"https://steamcommunity.com/stats/{self.app_id}/leaderboards/{self.lbid}/?xml=1" + self.entries = [] + while next_request_url: + xml = requests.get(next_request_url) + _bs = BeautifulSoup(xml.content, features="lxml") + for entry in _bs.find_all("entry"): + self.entries.append(Entry(entry)) + if _bs.response.entryend: + entry_end = int(_bs.response.entryend.text) + if entry_end < int(_bs.response.totalleaderboardentries.text): + next_request_url = f"https://steamcommunity.com/stats/{app_id}/leaderboards/{lbid}/?xml=1&start={entry_end + 1}" + else: + next_request_url = None + + def __repr__(self): + if self.name: + return f'' + else: + return f'' + + def find_entry(self, steam_id=None, *, rank=None): + if bool(steam_id) + bool(rank) > 1: + raise ValueError("You can only find an entry by 1 parameter.") + if steam_id is not None: + if not isinstance(steam_id, str): + raise ValueError("steam_id must be a str") + for entry in self.entries: + if entry.steam_id == steam_id: + return entry + else: + return None + elif rank is not None: + if not isinstance(rank, int): + raise ValueError("steam_id must be an int") + try: + return self.entries[rank - 1] + except IndexError: + return None + + +class Entry: + def __init__(self, soup): + self.steam_id = soup.steamid.text + self.score = int(soup.score.text) + self.rank = int(soup.rank.text) + self.ugcid = soup.ugcid.text + self.details = soup.details.text + + def __repr__(self): + return f""