# -*- coding: utf-8 -*-
""" Habitica API service interface.
"""
# Ensure backwards compatibility with Python 2
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals)
from builtins import *
import logging
from enum import Enum
import requests
from .errors import *
class HabiticaTaskTypes(Enum):
""" Habitica task types """
habits = 'habits'
dailies = 'dailys'
todos = 'todos'
rewards = 'rewards'
completed_todos = 'completedTodos'
class SpellIDs(Enum):
""" Spell/skill codes for casting.
*Mage*
fireball: "Burst of Flames"
mpheal: "Ethereal Surge"
earth: "Earthquake"
frost: "Chilling Frost"
*Warrior*
smash: "Brutal Smash"
defensiveStance: "Defensive Stance"
valorousPresence: "Valorous Presence"
intimidate: "Intimidating Gaze"
*Rogue*
pickPocket: "Pickpocket"
backStab: "Backstab"
toolsOfTrade: "Tools of the Trade"
stealth: "Stealth"
*Healer*
heal: "Healing Light"
protectAura: "Protective Aura"
brightness: "Searing Brightness"
healAll: "Blessing"
"""
burst_of_flames = 'fireball'
ethereal_surge = 'mpheal'
earthquake = 'earth'
chilling_frost = 'frost'
brutal_smash = 'smash'
defensive_stance = 'defensiveStance'
valorous_presence = 'valorousPresence'
intimidating_gaze = 'intimidate'
pickpocket = 'pickPocket'
backstab = 'backStab'
tools_of_the_trade = 'toolsOfTrade'
stealth = 'stealth'
heal = 'heal'
protective_aura = 'protectAura'
searing_brightness = 'brightness'
blessing = 'healAll'
[docs]class HabiticaService(object):
""" Habitica API service interface. """
def __init__(self, headers, base_url):
"""
Args:
headers (dict): HTTP headers.
base_url (str): The base URL for requests.
"""
self.__headers = headers
self.__base_url = base_url
self.__timeout = 10 # allow 10 seconds before timing out API calls
def __delete(self, command, params=None):
"""Utility wrapper around a HTTP DELETE"""
url = self.__base_url + command
logging.getLogger(__name__).debug('DELETE %s', url)
return requests.delete(
url,
params=params,
headers=self.__headers,
timeout=self.__timeout)
def __get(self, command, params=None):
"""Utility wrapper around a HTTP GET"""
url = self.__base_url + command
logging.getLogger(__name__).debug('GET %s', url)
return requests.get(
url,
params=params,
headers=self.__headers,
timeout=self.__timeout)
def __put(self, command, data):
"""Utility wrapper around a HTTP PUT"""
url = self.__base_url + command
logging.getLogger(__name__).debug('PUT %s', url)
return requests.put(
url,
headers=self.__headers,
data=data,
timeout=self.__timeout)
def __post(self, command, data=None):
"""Utility wrapper around a HTTP POST"""
url = self.__base_url + command
logging.getLogger(__name__).debug('PUT %s', url)
return requests.post(
url,
headers=self.__headers,
json=data,
timeout=self.__timeout)
@staticmethod
def __get_key(task):
""" Gets the key from the task ID or alias.
Preference is given to the ID.
Args:
task (dict): The task.
Returns:
str: The key
Raises:
ValueError: ID or alias not present in task.
"""
key = task.get('_id', None)
if not key:
key = task.get('alias', None)
if not key:
raise ValueError('The task must specify an id or alias')
return key
[docs] def is_server_up(self):
"""Check that the Habitica API is reachable and up
Returns:
bool: `True` if the server is reachable, otherwise `False`.
"""
response = self.__get('status')
if response.status_code == requests.codes.ok:
return response.json()['data']['status'] == 'up'
return False
[docs] def get_user(self):
"""Gets the authenticated user data.
Returns:
dict: The user data.
"""
response = self.__get('user')
response.raise_for_status()
return response.json()['data']
[docs] def get_stats(self):
"""Gets the authenticated user stats.
Returns:
dict: The stats.
"""
return self.get_user()['stats']
[docs] def get_tasks(self, task_type=None):
"""Gets all tasks for the current user.
Args:
task_type (HabiticaTaskTypes): The type of task to get.
Default is all tasks apart from completed todos.
Returns:
dict: The tasks.
"""
params = {'type': task_type.value} if task_type else {}
response = self.__get('tasks/user', params)
response.raise_for_status()
return response.json()['data']
[docs] def create_task(self, task, task_type=HabiticaTaskTypes.todos):
""" Creates a task.
Args:
task (dict): The task.
task_type (HabiticaTaskTypes): The type of task to create.
Default is to create a new todo. Only used if the task['type']
is empty or not present.
Returns:
dict: The new task as returned from the server.
"""
if not task.get('type', None):
_type = 'todo'
if task_type == HabiticaTaskTypes.dailies:
_type = 'daily'
elif task_type == HabiticaTaskTypes.habits:
_type = 'habit'
elif task_type == HabiticaTaskTypes.rewards:
_type = 'reward'
task['type'] = _type
response = self.__post('tasks/user', task)
response.raise_for_status()
return response.json()['data']
[docs] def create_tasks(self, tasks):
""" Creates multiple tasks.
Note that unlike HabiticaService.create_task, this method
**does not** check that the task type is valid.
Args:
task (list): The list of tasks.
Returns:
list: The new tasks as returned from the server.
"""
response = self.__post('tasks/user', tasks)
response.raise_for_status()
return response.json()['data']
[docs] def get_task(self, _id='', alias=''):
""" Gets a task.
If both task ID and alias are specified, then the ID is used.
Args:
_id (str): The task ID.
alias (str): The task alias.
Returns:
dict: The task, or None if the task is not found.
Raises:
ValueError
"""
key = _id if _id else alias
if not key:
raise ValueError('Neither ID or alias specified')
response = self.__get('tasks/{key}'.format(key=key))
if response.status_code == requests.codes.ok:
return response.json()['data']
else:
return None
[docs] def delete_task(self, task):
""" Delete a task.
Args:
task (dict): The task.
"""
response = self.__delete('tasks/{0}'.format(task['_id']))
response.raise_for_status()
[docs] def update_task(self, task):
""" Updates an existing task.
Args:
task (dict): The task.
Returns:
dict: The new task as returned from the server.
Raises:
ValueError: if neither an ID or alias are present in task.
"""
key = self.__get_key(task)
response = self.__put('tasks/{0}'.format(key), task)
response.raise_for_status()
return response.json()['data']
[docs] def score_task(self, task, direction='up'):
""" Score a task.
Args:
task (dict): the task to score.
direction (str): 'up' or 'down'
Returns:
dict: Habitica API response data.
Raises:
ValueError: invalid direction.
ValueError: missing ID or alias.
"""
key = self.__get_key(task)
response = self.__post(
'tasks/{0}/score/{1}'.format(key, direction),
data=None)
response.raise_for_status()
return response.json()['data']
[docs] def upsert_task(self, task, task_type=HabiticaTaskTypes.todos):
"""Upserts a task.
Existing tasks will be updated, otherwise a new task will be created.
Args:
task (dict): The task.
task_type (HabiticaTaskTypes): The type of task to create if a new
task is required. Can be overriden by an existing task['type']
value.
Returns:
dict: The new task as returned from the server.
Raises:
ValueError
"""
key = task.get('_id', None)
if not key:
key = task.get('alias', None)
if not key:
raise ValueError('The task must specify an id or alias')
# Does the task already exist?
if self.get_task(key):
logging.getLogger(__name__).debug('task %s exists, updating', key)
response = self.__put('tasks/{0}'.format(key), task)
response.raise_for_status()
return response.json()['data']
else:
logging.getLogger(__name__).debug(
'task %s not found, creating', key)
return self.create_task(task, task_type)
# I don't think the API lets me set partial user objects in this way.
# So I could get the entire user structure, swap the stats for the argument
# version, and then PUT that back. Or I can wait to see if I even need this
# method at all.
# def set_stats(self, stats):
# """Sets the authenticated user stats.
# ** Not implemented **
# Note that unlike the fine-grained set_[hp|mp|xp] methods,
# this method performs no sanity checking of values.
# Args:
# stats (dict): The stats to set. This can be a
# partial set of values.
# Returns: dictionary: The new stats, as returned by the server.
# Raises: NotImplementedError
# """
# raise NotImplementedError
# response = self.__put('user', {'stats': stats})
# if response.status_code == requests.codes.ok:
# return response.json()['data']['stats']
# return None
[docs] def set_hp(self, hp):
""" Sets the user's HP.
Args:
hp (float): The new HP value.
Returns:
float: The new HP value, extracted from the JSON response data.
"""
if hp > 50:
raise ArgumentOutOfRangeError("hp > 50")
if hp < 0:
raise ArgumentOutOfRangeError("hp < 0")
response = self.__put('user', {'stats.hp': hp})
response.raise_for_status()
return response.json()['data']['stats']['hp']
[docs] def set_mp(self, mp):
""" Sets the user's MP (mana points).
Args:
mp (float): The new MP value.
Returns:
float: The new MP value, extracted from the JSON response data.
"""
# max_mp = self.get_user()['stats']['maxMP']
# if mp > max_mp:
# raise ArgumentOutOfRangeError("mp > {0}".format(max_mp))
if mp < 0:
raise ArgumentOutOfRangeError("mp < 0")
response = self.__put('user', {'stats.mp': mp})
response.raise_for_status()
return response.json()['data']['stats']['mp']
[docs] def set_exp(self, exp):
""" Sets the user's XP (experience points).
Args:
exp (float): The new XP value.
Returns:
float: The new XP value, extracted from the JSON response data.
"""
if exp < 0:
raise ArgumentOutOfRangeError("exp < 0")
response = self.__put('user', {'stats.exp': exp})
response.raise_for_status()
return response.json()['data']['stats']['exp']
[docs] def set_lvl(self, lvl):
""" Sets the user's character level.
Note that XP will be reset to 0.
Args:
lvl (int): The new level.
Returns:
lvl: The new character level, extracted from the JSON response data.
"""
if lvl < 0:
raise ArgumentOutOfRangeError("lvl < 0")
response = self.__put('user', {'stats.lvl': lvl, 'stats.exp': 0})
response.raise_for_status()
return response.json()['data']['stats']['lvl']
[docs] def set_gp(self, gp):
""" Sets the user's gold (gp).
Args:
gp (float): The new gold value.
Returns:
float: The new gold value, extracted from the response data.
"""
if gp < 0:
raise ArgumentOutOfRangeError("gp < 0")
response = self.__put('user', {'stats.gp': gp})
response.raise_for_status()
return response.json()['data']['stats']['gp']
[docs] def create_tag(self, name):
""" Create a tag.
Args:
name (str): the tag name.
Returns:
dict: The new tag.
"""
response = self.__post('tags', data={'name': name})
response.raise_for_status()
return response.json()['data']
[docs] def delete_checklist_item(self, task_id, item_id):
""" Delete a checklist item.
Args:
task_id (str): The task ID.
item_id (str): The checklist item ID.
"""
response = self.__delete(
'tasks/{0}/checklist/{1}'.format(task_id, item_id))
response.raise_for_status()
[docs] def create_checklist_item(self, task_id, item):
""" Add a checklist item to the task.
Args:
task_id (str): The task ID.
item (dict): The new checklist item.
"""
response = self.__post(
'tasks/{0}/checklist'.format(task_id),
data=item)
response.raise_for_status()
[docs] def feed_pet(self, pet, food):
""" Feed a pet.
Args:
pet (str): The pet name.
food (str): The food.
Returns:
dict: The Habitica response data.
"""
response = self.__post('user/feed/{0}/{1}'.format(pet, food))
response.raise_for_status()
return response.json()
[docs] def hatch_pet(self, egg, potion):
""" Hatch a pet.
Args:
egg (str): The egg name.
potion (str): The potion name.
Returns:
dict: The Habitica response data.
"""
response = self.__post('user/hatch/{0}/{1}'.format(egg, potion))
response.raise_for_status()
return response.json()
[docs] def buy_armoire(self):
""" Buy an armoire item.
Returns:
dict: The Habitica response data.
"""
response = self.__post('user/buy-armoire')
response.raise_for_status()
return response.json()
[docs] def cast_skill_by_raw_spell_id(self, spellId, targetId=None):
""" Cast a skill using the raw Habitica API spell ID rather than the
enum.
Args:
spellId (str): The spell ID
targetId (UUID): Optional UUID of the spell target.
Required for targetted spells.
Returns:
dict: The Habitica response data.
"""
request = 'user/class/cast/{0}'.format(spellId)
if targetId:
request += '?targetId={0}'.format(targetId)
response = self.__post(request)
response.raise_for_status()
return response.json()
[docs] def cast_skill(self, spellId, targetId=None):
""" Cast a skill.
Args:
spellId (SpellIDs): The spell ID
targetId (UUID): Optional UUID of the spell target.
Required for targetted spells.
Returns:
dict: The Habitica response data.
"""
return self.cast_skill_by_raw_spell_id(spellId.value, targetId)