# -*- coding: utf-8 -*-
"""
Python API for Teambox
:copyright: (c) 2011 by Openlabs Technologies & Consulting (P) Limited
:license: BSD, see LICENSE for more details.
"""
import urllib
import urllib2
import base64
import json
__version__ = "0.1"
[docs]class RequestWithMethod(urllib2.Request):
"""
Implementation of urllib2.Request which also takes a method
"""
def __init__(self, *args, **kwargs):
self._method = kwargs.pop('method', None)
urllib2.Request.__init__(self, *args, **kwargs)
def get_method(self):
return (self._method if self._method \
else urllib2.Request.get_method(self)
)
[docs]class BaseAPI(object):
"""
Base implementation of the API
"""
#: The version of teambox api to connect to
api_version = "1"
def __init__(self, base_url=None, username=None, password=None):
"""
:param username: The username to use for Basic password auth
:param password: The password for Basic auth
:param base_url: URL of teambox installation. Defaults to the hosted
service at https://teambox.com
"""
if base_url is None:
base_url = "https://teambox.com"
authorization = base64.b64encode('%s:%s' % (username, password))
self.headers = {
'Accept': 'application/json',
'Authorization': "Basic %s" % authorization
}
self.base_url = base_url
[docs] def make_request(self, resource, data=None, method=None):
"""
Send a request
.. tip::
Use the method below which proxy this method as a restful
interface. See :meth:`post`, :meth:`get`, :meth:`delete` and
:meth:`put`
:param resource: resource path without / in beginning
"""
url_opener = urllib2.build_opener(
urllib2.HTTPCookieProcessor(),
urllib2.HTTPSHandler(),
)
url = '/'.join([self.base_url, "api/%s" % self.api_version, resource])
request = RequestWithMethod(url, data, self.headers, method=method)
return json.loads(url_opener.open(request).read())
[docs] def post(self, resource, data):
"""A proxy for :meth:`make_request` which sends a post to given uri
"""
return self.make_request(resource, urllib.urlencode(data))
[docs] def get(self, resource):
""" proxy for :meth:`make_request` which sends a GET to given uri
"""
return self.make_request(resource)
def delete(self, resource):
return self.make_reqeust(resource, method="DELETE")
def put(self, resource, data):
return self.make_request(resource, data, method="PUT")
[docs]class Organization(BaseAPI):
"""Organizations group together :class:`Projects` and :class:`User`s
(via :class:`Membership`).
"""
[docs] def create(self, data):
"""Creates a new organization
"""
return self.post("organizations", data)
[docs] def index(self):
"""Returns the most recent organizations you own or belong to.
By default external organizations* aren't included. They can be
included by adding external=true as a GET parameter
*External organization: An organization that owns a project the user
is in, but he's not on the organization.
"""
self.get("organizations")
[docs] def show(self, organization):
"""Returns the data for a given organization
"""
return self.get("organizations/%d" % organization)
[docs] def update(self, organization, data):
"""Updates an organization
"""
return self.put("organizations/%d" % organization, data)
[docs]class Membership(BaseAPI):
"""A membership links a User to an Organization.
To add people to an organization, make an Invitation.
"""
[docs] def destroy(self, organization, membership):
"""Destroys a member in the organization.
.. tip::
You need to be an administrator in the organization for this
to work.
"""
path = "organizations/%d/memberships/%d" % (organization, membership)
return self.delete(path)
[docs] def index(self, organization):
"""Returns the most recent people in the project.
"""
path = "organizations/%d/memberships" % organization
return self.get(path)
[docs] def show(self, organization, membership):
"""Returns the data for a person in the project
"""
path = "organizations/%d/memberships/%d" % (organization, membership)
return self.get(path)
[docs] def update(self, organization, membership, role):
"""Updates a membership in the project.
You need to be an administrator in the organization for this to work.
Roles are as follows:
* 10 External
* 20 Participant
* 30 Admin
"""
path = "organizations/%d/memberships/%d" % (organization, membership)
return self.put(path, {'role': role})
[docs]class Project(BaseAPI):
"""
Projects contain most of the objects present in Teambox.
"""
[docs] def create(self, data, organization=None):
"""
Routes:
* projects
* organizations/:organization_id/projects
"""
path = "organizations/%d/projects" % organization \
if organization else "projects"
return self.post(path, data)
[docs] def index(self, organization=None):
"""Returns the most recent projects you own or belong to.
.. tip::
You can also filter by organization by passing in organization_id.
"""
path = "organizations/%d/projects" % organization \
if organization else "projects"
return self.get(path)
[docs] def destroy(self, project, organization=None):
"""Destroys a project.
.. note::
You must be the owner in order to perform this action.
"""
path = "organizations/%d/projects/%d" % (organization, project) \
if organization else "projects/%d" % (project)
return self.delete(path)
[docs] def show(self, project, organization=None):
"""Returns the data for a given project
"""
path = "organizations/%d/projects/%d" % (organization, project) \
if organization else "projects/%d" % (project)
return self.get(path)
[docs] def update(self, project, data, organization=None):
"""Updates a project
"""
path = "organizations/%d/projects/%d" % (organization, project) \
if organization else "projects/%d" % (project)
return self.put(path, data)
[docs]class Person(BaseAPI):
"""
A person links a :class:`User` to a :class:`Project`.
To add people to a project, make an Invitation.
"""
[docs] def destroy(self, project, person):
"""Destroys a person in the project. You need to be an administrator
in the project for this to work.
"""
path = "projects/%d/people/%d" % (project, person)
return self.delete(path)
[docs] def index(self, project):
"""Returns the most recent people in the project.
"""
path = "projects/%d/people" % (project,)
return self.get(path)
[docs] def show(self, project, person):
"""Returns the data for a person in the project
"""
path = "projects/%d/people/%d" % (project, person)
return self.get(path)
[docs] def update(self, project, person, role):
"""Updates a person in the project.
.. note::
You need to be an administrator in the project for this to work.
Roles are as follows:
* 0 Observer
* 1 Commenter
* 2 Participant
* 3 Admin
"""
path = "projects/%d/people/%d" % (project, person)
return self.put(path, {'role': role})
#: A proxy object for :class:`Person` as teambox documentation list says
#: `People` instead of `Person`
People = Person
[docs]class Activity(BaseAPI):
"""An activity is a record of what happened in a :class:`Project`.
"""
[docs] def index(self, project=None, threads=None):
"""Returns the most recent activities in the project.
Related objects required to reconstruct a Teambox timeline are stored
in references.
By default all the activities will be returned, but by providing
`threads=True` as a parameter, comments inside threads won't be
returned.
This is ideal for apps that want to display a compact view of
activities (such as the collapsed view on the web version).
"""
path = "projects/%d/activities" % project if project \
else "activities"
if threads is not None:
path = "%s?threads=%s" % (path, (threads and "true" or "false"))
return self.get(path)
[docs] def show(self, activity, project=None):
"""Returns the data for an activity in the project.
"""
path = "projects/%d/activities/%d" % (project, activity) \
if project else "activities/%d" % activity
return self.get(path)
[docs]class Invitation(BaseAPI):
"""An Invitation invites a User to a Project, via email.
.. warning::
NOT IMPLEMENTED YET
"""
pass
[docs]class Conversation(BaseAPI):
"""Conversation is a group of comments.
It can also act as a thread in the project overview.
Comments belong to a Project.
.. warning::
NOT IMPLEMENTED YET
"""
pass
[docs]class TaskList(BaseAPI):
"""A task list is a collection of Tasks in a Project.
"""
[docs] def archive(self, project, task_list):
"""Archives the task list.
.. warning::
All tasks belonging to the task list will be updated and resolved.
"""
path = "projects/%d/task_lists/%d/archive" % (project, task_list)
return self.put(path, None)
def create(self, data, project=None):
path = "task_lists"
if project:
path = "projects/%d/%s" % (project, path)
return self.post(path, data)
[docs] def destroy(self, task_list, project=None):
"""Destroys a task list.
"""
path = "task_lists/%d" % task_list
if project:
path = "projects/%d/%s" % (project, path)
return self.delete(path)
[docs] def index(self, project=None, archived=None):
"""Returns the most recent task lists in a project.
.. tip::
To filter by archived or unarchived lists, pass in the optional
archived parameter. To view everything, simply omit the archived
parameter.
"""
path = "task_lists"
if project:
path = "projects/%d/%s" % (project, path)
if archived is not None:
path = "%s?archived=%s" % (archived and "true" or "false")
return self.get(path)
[docs] def reorder(self, project, order):
"""Reorders the task lists in a project according to the order each
task list id is presented in task_list_ids.
:param order: List of task_list ids in the order
"""
path = "projects/%d/task_lists/reorder" % project
return self.put(path, {'task_list_ids': ",".join(order)})
[docs] def show(self, task_list, project=None):
"""Returns the data for a task list.
"""
path = "task_lists/%d" % task_list
if project:
path = "projects/%d/%s" % (project, path)
return self.get(path)
[docs] def unarchive(self, project, task_list):
"""Unarchives the task list.
"""
path = "projects/%d/task_lists/%d/unarchive" % (project, task_list)
return self.put(path, None)
[docs] def update(self, task_list, data, project=None):
"""Updates the name, start date and end date of a task list.
"""
path = "task_lists/%d" % task_list
if project:
path = "projects/%d/%s" % (project, path)
return self.put(path, data)