Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
alexraskin committed Oct 13, 2023
0 parents commit ff50099
Show file tree
Hide file tree
Showing 11 changed files with 572 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.vscode
__pycache__
dist/
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# OverwatchPy

A Python wrapper for [https://overfast-api.tekrop.fr/](https://overfast-api.tekrop.fr/)

I wrote this very fast so it's not very good, but it works.

Please open a PR if you want to improve it or add more features.

## Requirements

- Python 3.11 (3.9+ should work)
- Poetry

## Installation

```bash
poetry install
```

## Usage

```python
from overwatchpy import Overwatch

search: Overwatch.player_search = Overwatch.player_search("twizy", "quickplay", "pc", "public")

for player in search:
print(player.name)

ow = Overwatch(battletag="Twizy#11358", gamemode="quickplay", platform="pc")

print(ow.maps())
```

## License
[MIT](https://choosealicense.com/licenses/mit/)
1 change: 1 addition & 0 deletions overwatchpy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .core import Overwatch
109 changes: 109 additions & 0 deletions overwatchpy/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from __future__ import absolute_import

import logging
from enum import Enum
from typing import Callable

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

from .const import locale
from .errors import OverwatchAPIError

logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
logger.setLevel(logging.DEBUG)

__version__: str = "0.0.1"

class EndPoint(Enum):
domain: str = "overfast-api.tekrop.fr"
scheme: str = "https"
api_base: str = "{scheme}://{domain}/".format(scheme=scheme, domain=domain)
player_url: str = api_base + "players" # with query params
all_player_data_url: str = api_base + "players/{battletag}"
player_summary_url: str = api_base + "players/{battletag}/summary"
player_stats_summary_url: str = api_base + "players/{battletag}/stats/summary"
player_career_url: str = api_base + "players/{battletag}/stats/career"
map_url: str = api_base + "maps"
gamemodes_url: str = api_base + "gamemodes"
heroes_url: str = api_base + "heroes"


class Client:
"""
The main class for the Overwatch API wrapper
"""

def __init__(
self,
use_retry: bool = True,
timeout: int = 30,
) -> None:
"""
Parameters
----------
use_retry : bool
default: True
Whether to retry on HTTP status codes 500, 502, 503, 504
timeout : int
default: 30
The timeout for the requests
"""
self.session: requests.session = requests.session()
self.session.headers["User-Agent"] = "overwatchpy/%s" % __version__
self.session.headers["Accept"] = "application/json"
self.timeout: int = timeout
if use_retry:
# Retry maximum 10 times, backoff on each retry
# Sleeps 1s, 2s, 4s, 8s, etc to a maximum of 120s between retries
# Retries on HTTP status codes 500, 502, 503, 504
retries: Retry = Retry(
total=10, backoff_factor=1, status_forcelist=[500, 502, 503, 504]
)
self.session.mount("https://", HTTPAdapter(max_retries=retries))

self.local: list = locale

def close(self):
self.session.close()

def request(
self,
path,
method: str = "GET",
params: dict = None,
headers: dict = None,
raw: bool = False,
allow_redirects: bool = True,
timeout: int = None,
) -> Callable[[dict], OverwatchAPIError]:
"""
Wrapper around requests.request()
"""
if not headers:
headers: dict = {}

if not params:
params: dict = {}

if not timeout:
timeout: int = self.timeout

response = self.session.request(
method,
path,
params=params,
headers=headers,
allow_redirects=allow_redirects,
timeout=self.timeout,
)
logger.debug("Response: %s", response)
if response.status_code != 200:
raise OverwatchAPIError(response.status_code, response.text)

if raw:
return response

return response.json()
15 changes: 15 additions & 0 deletions overwatchpy/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
locale: list = [
"de-de",
"en-gb",
"en-us",
"es-es",
"es-mx",
"fr-fr",
"it-it",
"ja-jp",
"ko-kr",
"pl-pl",
"pt-br",
"ru-ru",
"zh-tw",
]
183 changes: 183 additions & 0 deletions overwatchpy/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
from __future__ import absolute_import

import logging
import re
from typing import Callable, Literal, Optional

try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode

from .api import Client, EndPoint
from .errors import (InvalidBattletag, InvalidGamemode, InvalidOrderBy,
InvalidPrivacySettings, OverwatchAPIError,
PlatformNotRecognized)

logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

__version__ = "0.0.1"


class Overwatch(Client):

client = Client()

def __init__(
self,
battletag: str = None,
gamemode: Literal["quickplay", "competitive"] = None,
platform: Literal["pc", "console"] = None,
_data: Optional[dict] = None,
) -> None:
super().__init__()
self.battletag = battletag
self.gamemode = gamemode
self.platform = platform
if _data is not None:
self._data = _data
self.battletag = _data.get("player_id")
self.privacy = _data.get("privacy")
self.name = _data.get("name")

def ping(self) -> Callable[[dict], OverwatchAPIError]:
"""
Returns the ping
"""
return self.client.request(EndPoint.api_base.value)

@classmethod
def player_search(
cls,
battletag: str,
gamemode: Literal["quickplay", "competitive"],
platform: Literal["pc", "console"],
privacy: Literal["public", "private"],
order_by: Optional[
Literal[
"player_id:asc",
"player_id:desc",
"name:asc",
"name:desc",
"privacy:asc",
"privacy:desc",
]
] = "name:asc",
offset: Optional[int] = 0,
limit: Optional[int] = 20,
) -> Callable[[dict], OverwatchAPIError]:
"""
Search for a player
"""
reg = r"^[a-zA-Z0-9]{3,12}#[0-9]{4,5}$" # this could be improved
if battletag == re.match(reg, battletag):
raise InvalidBattletag("Invalid battletag")

if privacy not in ["public", "private"]:
raise InvalidPrivacySettings("Privacy must be either 'public', 'private'")

if platform not in ["pc", "console"]:
raise PlatformNotRecognized("Platform must be either 'pc', 'console'")

if gamemode not in ["quickplay", "competitive"]:
raise InvalidGamemode("Gamemode must be either 'quickplay', 'competitive'")

if order_by not in [
"player_id:asc",
"player_id:desc",
"name:asc",
"name:desc",
"privacy:asc",
"privacy:desc",
]:
raise InvalidOrderBy(
"Order by must be either 'player_id:asc', 'player_id:desc', 'name:asc', 'name:desc', 'privacy:asc', 'privacy:desc'"
)
params = {
"name": battletag,
"privacy": privacy,
"platform": platform,
"gamemode": gamemode,
"order_by": order_by,
"offset": offset,
"limit": limit,
}
response = cls.client.request(path=EndPoint.player_url.value, params=urlencode(params))

return [cls(_data=data) for data in response['results']]

def player_summary(self, battletag: Optional[str] = None) -> Callable[[dict], OverwatchAPIError]:
"""
Returns the player's summary
"""
if battletag is None:
battletag = self.battletag
return self.client.request(
EndPoint.player_summary_url.value.format(battletag=battletag)
)

def player_all_data(self, battletag: Optional[str] = None) -> Callable[[dict], OverwatchAPIError]:
"""
Returns the player's all data
"""
if battletag is None:
battletag = self.battletag
return self.client.request(
EndPoint.domain.all_player_data_url.value.format(battletag=self.battletag)
)

def player_stats(self, battletag: Optional[str] = None, gamemode: Optional[Literal],platform: Optional[Literal["pc", "console"]] = None) -> Callable[[dict], OverwatchAPIError]:
"""
Returns the player's stats
"""
params = {
"gamemode": self.gamemode,
"platform": self.platform,
}
return self.client.request(
EndPoint.player_stats_summary_url.value.format(battletag=self.battletag),
params=urlencode(params),
)

def player_career(self, hero: Optional[str]) -> Callable[[dict], OverwatchAPIError]:
"""
Returns the player's career
"""
params = {
"gamemode": self.gamemode,
"platform": self.platform,
"hero": "all-heroes" if hero is None else hero,
}
return self.client.request(
EndPoint.player_career_url.value.format(battletag=self.battletag),
params=urlencode(params),
)

def maps(self) -> Callable[[dict], OverwatchAPIError]:
"""
Returns the maps
"""
return self.client.request(EndPoint.map_url.value)

def gamemodes(self) -> Callable[[dict], OverwatchAPIError]:
"""
Returns the gamemodes
"""
return self.client.request(EndPoint.gamemodes_url.value)

def heroes(
self,
role: Literal["damage", "support", "tank"],
locale: Optional[str] = "en-us",
) -> Callable[[dict], OverwatchAPIError]:
"""
Returns the heroes
"""
if role not in ["damage", "support", "tank"]:
raise InvalidGamemode("Role must be either 'damage', 'support', 'tank'")
params = {
"role": role,
"locale": locale if locale in self.local else "en-us",
}
return self.client.request(EndPoint.heroes_url.value, params=urlencode(params))
44 changes: 44 additions & 0 deletions overwatchpy/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class OverwatchAPIError(Exception):
"""Base exception class for overwatchpy"""

...


class InvalidBattletag(Exception):
"""
Raise when 'battletag' key word argument is none
"""

...


class InvalidGamemode(Exception):
"""
Raise when 'gamemode' key word argument is not recognized
"""

...


class PlatformNotRecognized(Exception):
"""
Raise when 'platform' key word argument is not recognized
"""

...


class InvalidPrivacySettings(Exception):
"""
Raise when 'privacy' key word argument is not recognized
"""

...


class InvalidOrderBy(Exception):
"""
Raise when 'order_by' key word argument is not recognized
"""

...
Loading

0 comments on commit ff50099

Please sign in to comment.