Skip to content

Commit

Permalink
manager: added docs
Browse files Browse the repository at this point in the history
* new configuration field: country code
  • Loading branch information
zambonin committed Jan 5, 2018
1 parent 70c096c commit e73026a
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 12 deletions.
7 changes: 5 additions & 2 deletions README
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
A Python module that outputs detailed information about a Steam library to
a Google Drive spreadsheet using `gspread`.

Before using it, one must first setup a configuration JSON file with six items,
described below:
Before using it, one must first setup a configuration JSON file with a few
items, each described below:

Field | Description
----------------|-----------------------------------------------------------
Expand All @@ -17,6 +17,8 @@ described below:
| copied from its URL.
steam_login | Username used for login on Steam services.
steamid | The unique, 17-digit long identifier for a Steam account.
country_code | A two-letter code that represents the currency with which
| games were paid in. [4]

SteamCMD also needs to be installed, through your favourite package manager or
manually as per [3], and your credentials cached by logging in once manually
Expand All @@ -31,3 +33,4 @@ as an argument:
[1] https://steamcommunity.com/dev/apikey
[2] https://gspread.readthedocs.org/en/latest/oauth2.html
[3] https://developer.valvesoftware.com/wiki/SteamCMD
[4] https://partner.steamgames.com/doc/store/pricing/currencies
9 changes: 8 additions & 1 deletion manager/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""__main__.py
The main file for a Python module that outputs detailed information about a
Steam library to a Google Drive spreadsheet.
"""

from json import load, decoder
from sys import argv
from .data_composer import shape
Expand All @@ -9,7 +15,8 @@
try:
PRIVATE_DATA = load(open(argv[1]))
CELLS = shape(PRIVATE_DATA['steam_api_key'], PRIVATE_DATA['steamid'],
PRIVATE_DATA['steam_login'], PRIVATE_DATA['prices_file'])
PRIVATE_DATA['steam_login'], PRIVATE_DATA['prices_file'],
PRIVATE_DATA['country_code'])
upload(CELLS, PRIVATE_DATA['google_api_key'],
PRIVATE_DATA['spreadsheet_key'])
except (IndexError, FileNotFoundError, decoder.JSONDecodeError):
Expand Down
103 changes: 99 additions & 4 deletions manager/data_composer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""data_composer.py
Aggregates all functions related to transforming and organizing data into
standardized dictionary objects.
"""

import asyncio
from collections import ChainMap
from datetime import datetime
Expand All @@ -12,10 +18,38 @@


def parse_games_data(api_key, steamid):
"""
Reshapes the JSON content containing games associated to an account
returned by the Steam API.
Args:
api_key: a string representing the Steam web API key.
steamid: a string representing the identifier for a Steam account.
Returns:
A dictionary with game identifiers as keys and dictionaries with
further information, such as the game name and playtime, as values.
"""
return {game.pop('appid'): game for game in get_games(api_key, steamid)}


def parse_prices_data(api_key, steamid, file_path):
def parse_prices_data(api_key, steamid, file_path, c_code):
"""
Creates and/or updates a file containing information about how much one
has paid for each game on their account. This file is updated only if
there were new games added to the account.
Args:
api_key: a string representing the Steam web API key.
steamid: a string representing the identifier for a Steam account.
file_path: a string representing the absolute file path for the
prices file to be written to.
c_code: a string representing a two-letter country code.
Returns:
A dictionary with game identifiers as keys and dictionaries with
information about the original and paid prices as values.
"""
prices_file = load(open(file_path)) if path.isfile(file_path) else {}
games = {game['appid']: {'name': game['name']} for game in
get_games(api_key, steamid)}
Expand All @@ -25,14 +59,26 @@ def parse_prices_data(api_key, steamid, file_path):
for app in apps:
prices_file[app] = {
'paid': get_paid_price(games[app]['name']),
'orig': get_original_price(str(app)),
'orig': get_original_price(str(app), c_code),
}

dump(prices_file, open(file_path, 'w', encoding='utf8'), indent=2)
return {int(k): v for k, v in prices_file.items()}


def parse_achievements_data(api_key, steamid):
"""
Simplifies each game's achievements JSON request returned by the Steam API
and merges them together.
Args:
api_key: a string representing the Steam web API key.
steamid: a string representing the identifier for a Steam account.
Returns:
A dictionary with game identifiers as keys and dictionaries with
the achievement completion percentage for a game as values.
"""
games = parse_games_data(api_key, steamid)
achiev_dict = {appid: {'achv': ""} for appid in games.keys()}
apps = [k for k, v in games.items()
Expand All @@ -54,6 +100,18 @@ def parse_achievements_data(api_key, steamid):


def parse_licenses_data(login):
"""
Interprets SteamCMD's output when queried about licenses tied up to an
account.
Args:
login: a string representing the Steam login username.
Returns:
A dictionary with game identifiers as keys and dictionaries with
further information, such as the date of purchase for a license and
package identifier, as values.
"""
content = read_license_data(login)
indices = [i for i, line in enumerate(content) if "License" in line][1:]

Expand All @@ -72,6 +130,16 @@ def parse_licenses_data(login):


def get_paid_price(game):
"""
Input loop constraining what can be considered as a valid paid price for
a game.
Args:
game: a string with the game's name.
Returns:
A non-negative float number.
"""
valid = False
while not valid:
try:
Expand All @@ -86,21 +154,48 @@ def get_paid_price(game):


def show_icon(appid, icon):
"""
Constructs a valid Google Sheets' cell containing an image.
Args:
appid: unique identifier for any product on Steam's store.
icon: string containing the SHA-1 hash for the game's icon.
Returns:
A string that fits an image into a cell, maintaning aspect ratio.
"""
return ("=IMAGE(\"http://media.steampowered.com/steamcommunity/"
"public/images/apps/{}/{}.jpg\"; 1)").format(appid, icon)


def price_per_hour(game):
"""Ratio between paid price and playtime."""
return (game['paid'] / game['time']) if game['time'] else 0


def discount_info(game):
"""Percentage of paid price in relation to the original value."""
return (1 - (game['paid'] / game['orig'])) if game['orig'] else 0


def shape(api_key, steamid, login, prices_file):
def shape(api_key, steamid, login, file_path, c_code):
"""
Organizes and merges all dictionaries containing information about a game.
Args:
api_key: a string representing the Steam web API key.
steamid: a string representing the identifier for a Steam account.
login: a string representing the Steam login username.
file_path: a string representing the absolute file path for the
prices file to be written to.
c_code: a string representing a two-letter country code.
Returns:
A flattened list of the dictionaries, representing separate cells
of a spreadsheet.
"""
games = parse_games_data(api_key, steamid)
prices = parse_prices_data(api_key, steamid, prices_file)
prices = parse_prices_data(api_key, steamid, file_path, c_code)
achievements = parse_achievements_data(api_key, steamid)
licenses = parse_licenses_data(login)

Expand Down
76 changes: 71 additions & 5 deletions manager/data_conveyor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""data_conveyor.py
Aggregates all functions related to communication with external entities, such
as Google and Steam services.
* `gspread.authorize` logins to Google API using OAuth2 credentials.
* `oauth2client.service_account.ServiceAccountCredentials` handles the
creation of the object containing the credentials.
* `requests.get` issues a simple HTTP request to a web page.
"""

import asyncio
from subprocess import Popen, PIPE

Expand All @@ -9,40 +20,95 @@
from requests import get

def get_games(api_key, steamid):
"""
Requests which games an user owns on Steam.
Args:
api_key: a string representing the Steam web API key.
steamid: a string representing the identifier for a Steam account.
Returns:
A dictionary file containing information about games associated to
an user's account.
"""
url = "http://api.steampowered.com/IPlayerService/GetOwnedGames/v1/"
return get(url, params={
"key": api_key, "steamid": steamid,
"include_played_free_games": 1, "include_appinfo": 1,
}).json()['response']['games']


def get_original_price(game):
def get_original_price(appid, c_code):
"""
Gets the original price for a game through Steam store's open API.
Args:
appid: unique identifier for any product on Steam's store.
c_code: a string representing a two-letter country code.
Returns:
A non-negative float representing the original price for a game on
Steam's store if it is still available.
"""
url = "http://store.steampowered.com/api/appdetails"
output = get(url, params={"appids": game, "cc": "br"}).json()
output = get(url, params={"appids": appid, "cc": c_code}).json()

try:
return output[game]['data']['price_overview']['initial'] / 100
return output[appid]['data']['price_overview']['initial'] / 100
except KeyError:
return 0.0


async def get_achievements(api_key, steamid, appids):
"""
Gets achievements asynchronously for multiple games through Steam's API.
Args:
api_key: a string representing the Steam web API key.
steamid: a string representing the identifier for a Steam account.
appids: a list with unique identifier of games that may have stats.
Returns:
A list of JSON pre-formatted responses for all game identifiers.
"""
def get_game_achievs(_id):
url = ("http://api.steampowered.com/ISteamUserStats/"
"GetPlayerAchievements/v0001/?key={}&steamid={}&appid={}")
"""Requests a raw JSON containing info about a game's achievements,
identified by a unique identifier."""
return {_id : get(url.format(api_key, steamid, _id)).json()}

url = ("http://api.steampowered.com/ISteamUserStats/"
"GetPlayerAchievements/v0001/?key={}&steamid={}&appid={}")
loop = asyncio.get_event_loop()
return await asyncio.gather(*[
loop.run_in_executor(None, get_game_achievs, _id) for _id in appids])


def read_license_data(login):
"""
Queries about licenses by executing the SteamCMD binary.
Args:
login: a string representing the Steam login username.
Returns:
A list of strings containing the called command's stdout lines.
"""
cmd = "steamcmd +login {} +licenses_print +quit".format(login).split()
return [i.decode() for i in Popen(cmd, stdout=PIPE).stdout]


def upload(game_list, keyfile, ss_key):
"""
Performs authentication using OAuth2 and updates all cells at once,
resizing the spreadsheet if needed.
Args:
game_list: a list of strings containing information to be written to
spreadsheet cells.
keyfile: a file path pointing to a JSON file with the private key
used for authentication on Google's services.
ss_key: a string representing the unique key for a spreadsheet.
"""
credentials = ServiceAccountCredentials.from_json_keyfile_name(
keyfile, ['https://spreadsheets.google.com/feeds'])
worksheet = authorize(credentials).open_by_key(ss_key).sheet1
Expand Down

0 comments on commit e73026a

Please sign in to comment.