├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── osrs_api ├── __init__.py ├── const.py ├── grandexchange.py ├── hiscores.py ├── item.py ├── items_osrs.json ├── priceinfo.py ├── pricetrend.py └── skill.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py └── test_hiscore_api.py /.gitignore: -------------------------------------------------------------------------------- 1 | # VS Code 2 | .vscode/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Chasesc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build clean test_pub pub dev 2 | 3 | build: 4 | make clean 5 | python3 setup.py sdist bdist_wheel 6 | 7 | clean: 8 | rm -rf __pycache__/ 9 | rm -rf dist 10 | rm -rf build 11 | rm -rf python_osrsapi.egg-info/ 12 | 13 | test_pub: 14 | pip3 install --upgrade twine 15 | python3 -m twine upload --repository testpypi dist/* 16 | 17 | pub: 18 | pip3 install --upgrade twine 19 | python3 -m twine upload dist/* 20 | 21 | dev: 22 | pip3 install -e . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OSRS API Wrapper 2 | 3 | Allows simple access to Oldschool Runescape's API. Currently supports the only two APIs OSRS has. (Hiscores and GE) 4 | 5 | ### Install 6 | 7 | ``` 8 | pip install python-osrsapi 9 | ``` 10 | 11 | ### Dev Install 12 | 13 | ``` 14 | make dev 15 | ``` 16 | 17 | ### Hiscores 18 | 19 | ```python 20 | >>> from osrs_api.const import AccountType 21 | >>> from osrs_api import Hiscores 22 | >>> zezima = Hiscores('zezima') 23 | >>> zezima.skills 24 | {'attack': Skill(name=attack, rank=614026, level=76, xp=1343681), 'defence': ...} 25 | >>> zezima.skills['attack'].level 26 | 76 27 | >>> zezima.skills['attack'].xp_tnl() 28 | 131900 29 | >>> zezima.max_skill().name 30 | 'firemaking' 31 | >>> def maxed_skills(hiscore, skill): 32 | ... return hiscore.skills[skill].level == 99 33 | >>> zezima.filter(maxed_skills) 34 | {'firemaking': Skill(name=firemaking, rank=108780, level=99, xp=13034646)} 35 | >>> lynx = Hiscores('Lynx Titan') 36 | >>> mammal = Hiscores('mr mammal') 37 | >>> lynx > mammal 38 | True 39 | >>> iron_mammal = Hiscores('iron mammal', AccountType.IRONMAN) 40 | >>> iron_mammal.rank 41 | 1052 42 | ``` 43 | 44 | ### Grand Exchange 45 | 46 | ```python 47 | >>> from osrs_api import GrandExchange 48 | >>> from osrs_api import Item 49 | >>> whip_id = Item.get_ids('abyssal whip') 50 | >>> whip_id 51 | 4151 52 | >>> whip = GrandExchange.item(whip_id) 53 | >>> whip.description 54 | 'A weapon from the abyss.' 55 | >>> whip.price(), whip.is_mem 56 | (1648785, True) 57 | >>> thirty_days = whip.price_info.trend_30 58 | >>> thirty_days.trend, thirty_days.change 59 | ('negative', -18.0) 60 | >>> dagger_ids = Item.get_ids('rune dag') 61 | # If you enter a partial name, you will get a list of all possible matches. 62 | >>> dagger_ids 63 | [5696, 5678, 1229, 1213] 64 | # Names 65 | >>> [Item.id_to_name(id) for id in dagger_ids] 66 | ['Rune dagger(p++)', 'Rune dagger(p+)', 'Rune dagger(p)', 'Rune dagger'] 67 | >>> GrandExchange.item(dagger_ids[0]).description 68 | 'The blade is covered with a nasty poison.' 69 | ``` -------------------------------------------------------------------------------- /osrs_api/__init__.py: -------------------------------------------------------------------------------- 1 | from .grandexchange import GrandExchange 2 | from .hiscores import Hiscores, Minigame, Boss 3 | 4 | from .item import Item 5 | from .priceinfo import PriceInfo 6 | from .pricetrend import PriceTrend 7 | from .skill import Skill 8 | -------------------------------------------------------------------------------- /osrs_api/const.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | 3 | 4 | @unique 5 | class AccountType(Enum): 6 | NORMAL = "/m=hiscore_oldschool/index_lite.ws" 7 | IRONMAN = "/m=hiscore_oldschool_ironman/index_lite.ws" 8 | HARDCORE_IRONMAN = "/m=hiscore_oldschool_hardcore_ironman/index_lite.ws" 9 | ULTIMATE_IRONMAN = "/m=hiscore_oldschool_ultimate/index_lite.ws" 10 | DEADMAN = "/m=hiscore_oldschool_deadman/index_lite.ws" 11 | SEASONAL = "/m=hiscore_oldschool_seasonal/index_lite.ws" 12 | 13 | @classmethod 14 | def normal_types(cls): 15 | return [cls.NORMAL, 16 | cls.IRONMAN, 17 | cls.HARDCORE_IRONMAN, 18 | cls.ULTIMATE_IRONMAN] 19 | 20 | 21 | # Thanks to 22 | # http://mirekw.com/rs/RSDOnline/Guides/guide.aspx?file=Experience%20formula.html 23 | # for the formula 24 | def _build_xp_table(): 25 | table = [0] 26 | xp = 0 27 | 28 | for level in range(1, 99): 29 | diff = int(level + 300 * (2 ** (level / 7.0))) 30 | xp += diff 31 | table.append(xp // 4) 32 | 33 | return table 34 | 35 | 36 | # index retrives the amount of xp required for level index + 1 37 | XP_TABLE = _build_xp_table() 38 | 39 | SKILLS = [ 40 | "attack", 41 | "defence", 42 | "strength", 43 | "hitpoints", 44 | "ranged", 45 | "prayer", 46 | "magic", 47 | "cooking", 48 | "woodcutting", 49 | "fletching", 50 | "fishing", 51 | "firemaking", 52 | "crafting", 53 | "smithing", 54 | "mining", 55 | "herblore", 56 | "agility", 57 | "thieving", 58 | "slayer", 59 | "farming", 60 | "runecrafting", 61 | "hunter", 62 | "construction", 63 | ] 64 | 65 | MINIGAMES = [ 66 | "League Points", 67 | "Deadman Points", 68 | "Bounty Hunter - Hunter", 69 | "Bounty Hunter - Rogue", 70 | "Bounty Hunter (Legacy) - Hunter", 71 | "Bounty Hunter (Legacy) - Rogue", 72 | "Clue Scrolls (all)", 73 | "Clue Scrolls (beginner)", 74 | "Clue Scrolls (easy)", 75 | "Clue Scrolls (medium)", 76 | "Clue Scrolls (hard)", 77 | "Clue Scrolls (elite)", 78 | "Clue Scrolls (master)", 79 | "LMS - Rank", 80 | "PvP Arena - Rank", 81 | "Soul Wars Zeal", 82 | "Rifts closed", 83 | ] 84 | 85 | BOSSES = [ 86 | "Abyssal Sire", 87 | "Alchemical Hydra", 88 | "Artio", 89 | "Barrows Chests", 90 | "Bryophyta", 91 | "Callisto", 92 | "Cal'varion", 93 | "Cerberus", 94 | "Chambers of Xeric", 95 | "Chambers of Xeric: Challenge Mode", 96 | "Chaos Elemental", 97 | "Chaos Fanatic", 98 | "Commander Zilyana", 99 | "Corporeal Beast", 100 | "Crazy Archaeologist", 101 | "Dagannoth Prime", 102 | "Dagannoth Rex", 103 | "Dagannoth Supreme", 104 | "Deranged Archaeologist", 105 | "Duke Sucellus", 106 | "General Graardor", 107 | "Giant Mole", 108 | "Grotesque Guardians", 109 | "Hespori", 110 | "Kalphite Queen", 111 | "King Black Dragon", 112 | "Kraken", 113 | "Kree'Arra", 114 | "K'ril Tsutsaroth", 115 | "Mimic", 116 | "Nex", 117 | "Nightmare", 118 | "Phosani's Nightmare", 119 | "Obor", 120 | "Phantom Muspah", 121 | "Sarachnis", 122 | "Scorpia", 123 | "Skotizo", 124 | "Spindel", 125 | "Tempoross", 126 | "The Gauntlet", 127 | "The Corrupted Gauntlet", 128 | "The Leviathan", 129 | "The Whisperer", 130 | "Theatre of Blood", 131 | "Theatre of Blood: Hard Mode", 132 | "Thermonuclear Smoke Devil", 133 | "Tombs of Amascut", 134 | "Tombs of Amascut: Expert Mode", 135 | "TzKal-Zuk", 136 | "TzTok-Jad", 137 | "Vardorvis", 138 | "Venenatis", 139 | "Vet'ion", 140 | "Vorkath", 141 | "Wintertodt", 142 | "Zalcano", 143 | "Zulrah", 144 | ] 145 | 146 | SKILLS_AMT = len(SKILLS) 147 | MINIGAMES_AMT = len(MINIGAMES) 148 | BOSSES_AMT = len(BOSSES) 149 | 150 | BASE_URL = "http://services.runescape.com" 151 | 152 | BASE_URL_GE = BASE_URL + "/m=itemdb_oldschool/" 153 | GE_BY_ID = BASE_URL_GE + "api/catalogue/detail.json?item=" 154 | 155 | GE_ICON = BASE_URL_GE + "obj_sprite.gif?id=" 156 | GE_LARGE_ICON = BASE_URL_GE + "obj_big.gif?id=" 157 | 158 | OSBUDDY_PRICE_URI = "http://api.rsbuddy.com/grandExchange?a=guidePrice&i=" 159 | -------------------------------------------------------------------------------- /osrs_api/grandexchange.py: -------------------------------------------------------------------------------- 1 | import urllib.request 2 | import json 3 | import warnings 4 | 5 | from . import const 6 | from .item import Item 7 | from .pricetrend import PriceTrend 8 | from .priceinfo import PriceInfo 9 | 10 | 11 | class GrandExchange(object): 12 | # OSBuddy is an unofficial API, but it is more accurate than the offical API. 13 | # They give more significant figures than the offical API and the values 14 | # are closer to the actively traded prices. 15 | @staticmethod 16 | def _osbuddy_price(id): 17 | # TODO: remove this. OSBuddy no longer has a public API? 18 | warnings.warn( 19 | "OSBuddy no longer provides a public API. This functionality will be removed.", 20 | DeprecationWarning, 21 | stacklevel=2, 22 | ) 23 | 24 | osb_uri = const.OSBUDDY_PRICE_URI + str(id) 25 | osb_price = None 26 | try: 27 | osb_response = urllib.request.urlopen(osb_uri) 28 | 29 | osb_data = osb_response.read() 30 | encoding = osb_response.info().get_content_charset("utf-8") 31 | osb_response.close() 32 | 33 | osb_json_data = json.loads(osb_data.decode(encoding)) 34 | osb_price = osb_json_data["overall"] 35 | except Exception: 36 | pass # oh well, price will just be less accurate 37 | 38 | return osb_price 39 | 40 | @staticmethod 41 | def item(id, try_osbuddy=False): 42 | uri = const.GE_BY_ID + str(id) 43 | 44 | try: 45 | response = urllib.request.urlopen(uri) 46 | except urllib.error.HTTPError: 47 | raise Exception("Unable to find item with id %d." % id) 48 | 49 | data = response.read() 50 | encoding = response.info().get_content_charset("utf-8") 51 | response.close() 52 | 53 | osb_price = None 54 | if try_osbuddy: 55 | osb_price = GrandExchange._osbuddy_price(id) 56 | 57 | json_data = json.loads(data.decode(encoding))["item"] 58 | 59 | name = json_data["name"] 60 | description = json_data["description"] 61 | is_mem = bool(json_data["members"]) 62 | type = json_data["type"] 63 | type_icon = json_data["typeIcon"] 64 | 65 | # price info/trends 66 | current = json_data["current"] 67 | today = json_data["today"] 68 | day30 = json_data["day30"] 69 | day90 = json_data["day90"] 70 | day180 = json_data["day180"] 71 | 72 | curr_trend = PriceTrend(current["price"], current["trend"], None) 73 | trend_today = PriceTrend(today["price"], today["trend"], None) 74 | trend_30 = PriceTrend(None, day30["trend"], day30["change"]) 75 | trend_90 = PriceTrend(None, day90["trend"], day90["change"]) 76 | trend_180 = PriceTrend(None, day180["trend"], day180["change"]) 77 | 78 | price_info = PriceInfo( 79 | curr_trend, trend_today, trend_30, trend_90, trend_180, osb_price 80 | ) 81 | 82 | return Item(id, name, description, is_mem, type, type_icon, price_info) 83 | 84 | 85 | def main(): 86 | abyssal_whip_id = 4151 87 | whip = GrandExchange.item(abyssal_whip_id) 88 | print(whip.name, whip.description, whip.price(), sep="\n") 89 | 90 | rune_axe_id = Item.get_ids("rune axe") 91 | rune_axe = GrandExchange.item(rune_axe_id) 92 | print(rune_axe.name, rune_axe.description, rune_axe.price(), sep="\n") 93 | 94 | 95 | if __name__ == "__main__": 96 | main() 97 | -------------------------------------------------------------------------------- /osrs_api/hiscores.py: -------------------------------------------------------------------------------- 1 | import re 2 | import urllib.request 3 | 4 | from collections import namedtuple 5 | from urllib.parse import quote 6 | 7 | from . import const 8 | from .skill import Skill 9 | 10 | Minigame = namedtuple("Minigame", ["name", "rank", "score"]) 11 | Boss = namedtuple("Boss", ["name", "rank", "kills"]) 12 | 13 | 14 | class Hiscores(object): 15 | def __init__(self, username, account_type=None): 16 | self.username = username 17 | self._type = account_type 18 | self._api_response = None 19 | 20 | if self._type is None: 21 | self._find_account_type() # _type is set inside _find_account_type 22 | 23 | self.rank, self.total_level, self.total_xp = -1, -1, -1 24 | 25 | self.skills = {} 26 | self.minigames = {} 27 | self.bosses = {} 28 | 29 | self.update() 30 | 31 | def update(self): 32 | # In the default case (account_type=None), we already have this 33 | # information. We don't need to do it again 34 | if self._api_response is None: 35 | self._api_response = self._get_api_data() 36 | self._set_data() 37 | self._api_response = None 38 | 39 | @property 40 | def _url(self): 41 | return const.BASE_URL + self._type.value + "?player=" 42 | 43 | def _find_account_type(self): 44 | ''' 45 | The RS API does not tell you the account type, so we have to find out here. 46 | Try in the order of special type -> normal account, so we find the correct type 47 | ''' 48 | 49 | for possible_type in reversed(const.AccountType.normal_types()): 50 | try: 51 | self._type = possible_type 52 | self._api_response = self._get_api_data() 53 | return 54 | except BaseException: 55 | pass 56 | 57 | self._raise_bad_username() 58 | 59 | def _raise_bad_username(self): 60 | raise Exception("Unable to find %s in the hiscores." % self.username) 61 | 62 | def _get_api_data(self): 63 | try: 64 | safe_url = "%s%s" % (self._url, quote(self.username)) 65 | url = urllib.request.urlopen(safe_url) 66 | except urllib.error.HTTPError: 67 | self._raise_bad_username() 68 | 69 | data = url.read() 70 | url.close() 71 | 72 | return data.decode().split("\n") 73 | 74 | def _set_data(self): 75 | """ 76 | The RS api is not documented and is given as a list of numbers on multiple lines. These lines are: 77 | 78 | "overall_rank, total_level, total_xp" 79 | "skill_rank, skill_level, skill_xp" for all skills 80 | "minigame_rank, minigame_score" for all minigames 81 | "boss_rank, boss_kills" for all bosses 82 | 83 | If a player is unranked for any of these categories, there is a value of -1 in that row. 84 | 85 | example: https://secure.runescape.com/m=hiscore_oldschool_ironman/index_lite.ws?player=lelalt 86 | """ 87 | 88 | # Get all the skills, minigames, or bosses 89 | def _get_api_chunk(cls, *, names, start_index): 90 | """ 91 | cls: Skill, Minigame, or Boss - Type of the chunk 92 | names: List[str] - a list of all the (Skill, Minigame, or Boss) names in the order the API returns them. 93 | (const.SKILLS, const.MINIGAMES, or const.BOSSES) 94 | start_index: The index into self._api_response where the chunk begins 95 | """ 96 | 97 | chunk = {} 98 | 99 | for i, name in enumerate(names, start=start_index): 100 | if (self._type is not const.AccountType.SEASONAL and name == 101 | "League Points"): 102 | continue 103 | 104 | # The API only returns integers 105 | row_data = [int(col) 106 | for col in self._api_response[i].split(",")] 107 | 108 | chunk[name] = cls(name, *row_data) 109 | 110 | return chunk 111 | 112 | overall_data = self._api_response[0].split(",") 113 | self.rank, self.total_level, self.total_xp = ( 114 | int(overall_data[0]), 115 | int(overall_data[1]), 116 | int(overall_data[2]), 117 | ) 118 | 119 | self.skills = _get_api_chunk(Skill, names=const.SKILLS, start_index=1) 120 | self.minigames = _get_api_chunk( 121 | Minigame, names=const.MINIGAMES, start_index=1 + const.SKILLS_AMT 122 | ) 123 | self.bosses = _get_api_chunk( 124 | Boss, 125 | names=const.BOSSES, 126 | start_index=1 + const.SKILLS_AMT + const.MINIGAMES_AMT, 127 | ) 128 | 129 | def max_skill(self, method="xp"): 130 | ninf = -float("inf") 131 | max_skill = Skill("attack", xp=ninf, rank=ninf) 132 | for skill in self.skills.values(): 133 | if getattr(skill, method) > getattr(max_skill, method): 134 | max_skill = skill 135 | 136 | return max_skill 137 | 138 | def min_skill(self, method="xp"): 139 | inf = float("inf") 140 | min_skill = Skill("attack", xp=inf, rank=inf) 141 | for skill in self.skills.values(): 142 | if getattr(skill, method) < getattr(min_skill, method): 143 | min_skill = skill 144 | 145 | return min_skill 146 | 147 | def closest_skill(self): 148 | closest = Skill("attack") 149 | closest_xp_tnl = float("inf") 150 | for skill in self.skills.values(): 151 | if skill.xp_tnl() < closest_xp_tnl: 152 | closest_xp_tnl, closest = skill.xp_tnl(), skill 153 | 154 | return closest 155 | 156 | def skills_under(self, value, method="level"): 157 | def under(hiscore, s): 158 | return getattr(hiscore.skills[s], method) < value 159 | 160 | return self.filter(under) 161 | 162 | def skills_over(self, value, method="level"): 163 | def over(hiscore, s): 164 | return getattr(hiscore.skills[s], method) > value 165 | 166 | return self.filter(over) 167 | 168 | def filter(self, predicate): 169 | return {s: self.skills[s] for s in self.skills if predicate(self, s)} 170 | 171 | def __str__(self): 172 | attrs = [ 173 | ("Rank", self.rank), 174 | ("Total Level", self.total_level), 175 | ("Total XP", self.total_xp), 176 | ] 177 | return ( 178 | "\n".join(f"{name}: {str(item)}" for name, item in attrs) 179 | + "\n" 180 | + "\n".join(str(self.skills[skill]) for skill in self.skills) 181 | ) 182 | 183 | def __repr__(self): 184 | return f"Hiscores({self.username}, type={self._type.name})" 185 | 186 | # == 187 | def __eq__(self, other): 188 | if isinstance(other, Hiscores): 189 | return self.rank == other.rank 190 | return False 191 | 192 | # != 193 | def __ne__(self, other): 194 | equals = self.__eq__(other) 195 | return not equals 196 | 197 | # < 198 | def __lt__(self, other): 199 | if isinstance(other, Hiscores): 200 | return self.rank > other.rank # flipped because lower rank is better 201 | return False 202 | 203 | # > 204 | def __gt__(self, other): 205 | if isinstance(other, Hiscores): 206 | return self.rank < other.rank # flipped because lower rank is better 207 | return False 208 | 209 | # <= 210 | def __le__(self, other): 211 | return self.__eq__(other) or self.__lt__(other) 212 | 213 | # >= 214 | def __ge__(self, other): 215 | return self.__eq__(other) or self.__gt__(other) 216 | 217 | 218 | def main(): 219 | top = Hiscores("Lelalt", const.AccountType.IRONMAN) 220 | print(str(top)) 221 | 222 | 223 | if __name__ == "__main__": 224 | main() 225 | -------------------------------------------------------------------------------- /osrs_api/item.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from pathlib import Path 4 | from . import const 5 | 6 | 7 | class Item(object): 8 | _items = {} 9 | _name_to_id = {} 10 | 11 | def __init__(self, id, name, description, is_mem, type, type_icon, price_info): 12 | self.id = id 13 | self.name = name 14 | self.description = description 15 | self.is_mem = is_mem 16 | self.type = type 17 | self.type_icon = type_icon 18 | self.price_info = price_info 19 | 20 | self.icon = const.GE_ICON + str(self.id) 21 | self.large_icon = const.GE_LARGE_ICON + str(self.id) 22 | 23 | def price(self): 24 | return self.price_info.price() 25 | 26 | @staticmethod 27 | def id_to_name(id): 28 | if not Item._items: 29 | Item._load_data() 30 | 31 | try: 32 | return Item._items[str(id)]["name"] 33 | except KeyError: 34 | return None 35 | 36 | @staticmethod 37 | def get_ids(name): 38 | if not Item._items: 39 | Item._load_data() 40 | 41 | try: 42 | return Item._name_to_id[name.lower()] 43 | except KeyError: 44 | matches = [] 45 | for item in Item._name_to_id: 46 | if name in item: 47 | matches.append(Item._name_to_id[item]) 48 | 49 | return matches 50 | 51 | @staticmethod 52 | def _load_data(): 53 | with open(Path(__file__).parent / "items_osrs.json") as file: 54 | Item._items = json.load(file) 55 | for id in Item._items: 56 | n = Item._items[id]["name"].lower() 57 | tradeable = Item._items[id]["tradeable"] 58 | 59 | if tradeable: 60 | Item._name_to_id[n] = int(id) 61 | 62 | 63 | def main(): 64 | print(Item.get_ids("abyssal whip")) # exact match 65 | print(Item.get_ids("abyssal")) # returns a list of items that contain 'abyssal' 66 | print(Item.get_ids("rune axe")) # exact match 67 | 68 | 69 | if __name__ == "__main__": 70 | main() 71 | -------------------------------------------------------------------------------- /osrs_api/priceinfo.py: -------------------------------------------------------------------------------- 1 | class PriceInfo(object): 2 | def __init__( 3 | self, curr_trend, trend_today, trend_30, trend_90, trend_180, osbuddy_price 4 | ): 5 | self.curr_trend = curr_trend 6 | self.trend_today = trend_today 7 | self.trend_30 = trend_30 8 | self.trend_90 = trend_90 9 | self.trend_180 = trend_180 10 | self.osbuddy_price = osbuddy_price 11 | 12 | def price(self): 13 | if not self.osbuddy_price: 14 | return self.curr_trend.price 15 | return self.osbuddy_price 16 | -------------------------------------------------------------------------------- /osrs_api/pricetrend.py: -------------------------------------------------------------------------------- 1 | class PriceTrend(object): 2 | 3 | _money_shorthands = {"k": 1000, "m": 1000000, "b": 1000000000} 4 | 5 | def __init__(self, price, trend, change): 6 | self.price = self._extract_price(price) 7 | self.trend = trend 8 | self.change = self._extract_change(change) 9 | 10 | def _extract_price(self, price): 11 | if price is None: 12 | return None 13 | 14 | price = str(price).replace(" ", "").replace(",", "") 15 | 16 | last = price[-1] # Get the last character 17 | # check if this price is in shorthand notation. EX. '1.6m' 18 | if last in PriceTrend._money_shorthands.keys(): 19 | # if it is, convert it to be a floating point num. 20 | # EX. '1.6m' -> 1000000 * 1.6 -> 1600000.0 21 | return PriceTrend._money_shorthands[last] * float(price[:-1]) 22 | 23 | return float(price) 24 | 25 | def _extract_change(self, change): 26 | if change is None: 27 | return None 28 | 29 | return float(change[:-1]) 30 | 31 | def __str__(self): 32 | v = vars(self) 33 | details = ", ".join([f"{n}={v}" for n, v in v.items() if v is not None]) 34 | return f"PriceTrend({details})" 35 | 36 | def __repr__(self): 37 | return self.__str__() 38 | 39 | 40 | def main(): 41 | pt = PriceTrend(" 1,320k", "neutral", "+5.0%") 42 | print(str(pt)) 43 | 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /osrs_api/skill.py: -------------------------------------------------------------------------------- 1 | from . import const 2 | 3 | 4 | class Skill(object): 5 | def __init__(self, name, rank=0, level=0, xp=0): 6 | if name not in const.SKILLS: 7 | raise Exception("%s is not a valid skill." % name) 8 | 9 | self.name = name 10 | self.rank = rank 11 | self.level = level 12 | self.xp = xp 13 | 14 | # TODO: Add virtual levels to the XP_TABLE 15 | # xp to next level 16 | def xp_tnl(self): 17 | if self.level >= 99: 18 | return 0 # already maxed, no real next level 19 | return const.XP_TABLE[self.level] - self.xp 20 | 21 | def xp_to(self, level): 22 | if level > 99: 23 | return 0 # same reason as above. 24 | return const.XP_TABLE[level - 1] - self.xp 25 | 26 | def __str__(self): 27 | return f"Skill(name={self.name}, rank={self.rank}, level={self.level}, xp={self.xp})" 28 | 29 | def __repr__(self): 30 | return self.__str__() 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="python_osrsapi", 8 | version="0.0.6", 9 | author="Chasesc", 10 | description="Oldschool Runescape API wrapper", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="https://github.com/Chasesc/OSRS-API-Wrapper", 14 | packages=setuptools.find_packages(exclude=["tests"]), 15 | keywords=[ 16 | "osrs", 17 | "runescape", 18 | "jagex", 19 | "api", 20 | "grandexchange", 21 | "hiscores", 22 | ], 23 | classifiers=[ 24 | "Development Status :: 3 - Alpha", 25 | "Intended Audience :: Developers", 26 | "Programming Language :: Python :: 3", 27 | "License :: OSI Approved :: MIT License", 28 | "Operating System :: OS Independent", 29 | ], 30 | python_requires='>=3.6', # due to f-strings 31 | project_urls= { 32 | 'Bug Reports': 'https://github.com/Chasesc/OSRS-API-Wrapper/issues', 33 | 'Source': 'https://github.com/Chasesc/OSRS-API-Wrapper/', 34 | }, 35 | package_data={ 36 | "" : ["*.json"] 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chasesc/OSRS-API-Wrapper/675226f9d2a5d163c045b264d1020dd363b6b410/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_hiscore_api.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from osrs_api.hiscores import Hiscores 4 | from osrs_api.const import SKILLS_AMT, MINIGAMES_AMT, BOSSES_AMT 5 | 6 | 7 | class TestHiscore(unittest.TestCase): 8 | def test_api_length(self): 9 | """ 10 | Check to see if the API returns the same number of skills, minigames, and bosses 11 | that are mentioned in const.py. If this test fails, the items in const.py 12 | need to be updated. 13 | """ 14 | score = Hiscores(username="Lelalt") 15 | expected_num_api_elements = SKILLS_AMT + MINIGAMES_AMT + BOSSES_AMT 16 | api_data = score._get_api_data() 17 | 18 | # Remove empty string and overall values since they're not used 19 | len_api_data = len(api_data) - 2 20 | 21 | self.assertEqual(expected_num_api_elements, len_api_data) 22 | --------------------------------------------------------------------------------