by David Barragán Merino / @bameda
bameda on Github
@bameda on Twitter
#FFF8E7 at Kaleidos Open Source
Grouchy Smurf at Taiga Agile
...draw graphics about the statistics of the issues of my project
What about the API authentication?
How can I get the data?
How can I draw the graphs?
Token Based Authentication: JSON Web Tokens (JWT)
To authenticate requests an http header called "Authorization" should be added. Its format should be:
Authorization: Bearer ${AUTH_TOKEN}
This token ${AUTH_TOKEN} can be received through the login API.
To login a user send a POST request containing the following data:
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"type": "normal",
"username": "'${USERNAME}'",
"password": "'${PASSWORD}'"
}' \
https://api.taiga.io/api/v1/auth
When the login is successful, the HTTP response is a 200 OK and the response body is a JSON user auth detail object
{
"id": 7,
"is_active": true,
"username": "beta.tester",
"email": "beta.testing@taiga.io",
"full_name": "Beta testing",
"auth_token": "eyJ1c2VyX2F1dGhlbnRpY2F0aW9uX2lkIjo3fq:1XmPud:LKXVD9Z0rmHJjiyy0m4YaaHlQS1",
(...)
}
To get a project issue stats send a GET request specifying the project_id in the url:
curl -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${AUTH_TOKEN}" \
https://api.taiga.io/api/v1/projects/1/issues_stats
The HTTP response is a 200 OK and the response body is a JSON project issue stats object.
{
"total_issues": 668,
"opened_issues": 108,
"closed_issues": 560,
"issues_per_type": {
"1": {
"id": 1,
"name": "Bug",
"color": "#f57900",
"count": 501
},
(...)
},
"issues_per_status": {
"1": {
"id": 1,
"name": "New",
"color": "#8C2318",
"count": 90
},
(...)
},
"issues_per_owner": {
"11": {
"id": 11,
"name": "Anler Hern\u00e1ndez Peral",
"username": "anler.hernandez",
"color": "#8f0030",
"count": 1
},
(...)
},
"issues_per_assigned_to": {
"0": {
"id": 0,
"name": "Unassigned",
"username": "Unassigned",
"color": "black",
"count": 185
},
(...)
},
"issues_per_priority": {
"1": {
"id": 1,
"name": "Low",
"color": "#888a85",
"count": 50
},
(...)
},
"issues_per_severity": {
"1": {
"id": 1,
"name": "Wishlist",
"color": "#888a85",
"count": 9
},
(...)
},
"last_four_weeks_days": {
"by_open_closed": {
"closed": [8, 3, 2, 0, (...)],
"open": [7, 3, 2, 0, (...)]
},
"by_priority": {
"1": {
"id": 1,
"name": "Low",
"color": "#888a85",
"data": [18, 17, 17, (...)]
},
},
"by_severity": {
"1": {
"id": 1,
"name": "Wishlist",
"color": "#888a85",
"data": [8, 8, 8, (...)]
},
},
"by_status": {(...)}
},
}
To list projects send a GET request with the following parameters:
curl -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${AUTH_TOKEN}" \
https://api.taiga.io/api/v1/projects
The HTTP response is a 200 OK and the response body is a JSON list of project list entry objects
[
{
"anon_permissions": [],
"created_date": "2014-09-16T15:39:49+0000",
"creation_template": 1,
"default_issue_status": 574,
"default_issue_type": 127,
"default_points": 977,
"default_priority": 245,
"default_severity": 408,
"default_task_status": 411,
"default_us_status": 352,
"description": "Taiga",
"i_am_owner": true,
"id": 87,
"is_backlog_activated": false,
"is_issues_activated": true,
"is_kanban_activated": true,
"is_private": false,
"is_liked": true,
"is_watched": true,
"is_wiki_activated": true,
"members": [
2,
120,
(...)
],
"modified_date": "2014-10-29T07:35:38+0000",
"my_permissions": [
"admin_project_values",
"view_tasks",
"view_milestones",
"view_project",
(...)
],
"name": "AIL",
"owner": 2,
"public_permissions": [],
"slug": "ail",
"likes": 3,
"tags": null,
"tags_colors": {
"api": "#ce5c00",
"cuidador": "#204a87",
"gestor": "#73d216",
(...)
},
"total_milestones": 3,
"total_story_points": 20.0,
"videoconferences": "appear-in",
"videoconferences_salt": null,
"voters": 1,
"watchers": [
7,
(...)
]
},
(...)
]
The results can be filtered using the following parameters:
curl -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${AUTH_TOKEN}" \
https://api.taiga.io/api/v1/projects?member=1
The results can be ordered using the order_by parameter with the values:
curl -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${AUTH_TOKEN}" \
https://api.taiga.io/api/v1/projects?member=1&order_by=memberships__user_order
HTTP for Humans
#!/usr/bin/env python
from urllib.parse import urljoin
import sys
import copy
import getpass
from datetime import datetime, timedelta
import requests
import pygal
BASE_HEADERS = {
"content-type": "application/json; charset: utf8",
"X-DISABLE-PAGINATION": "true",
}
API_HOST = "https://api.taiga.io/"
URLS = {
"auth": "/api/v1/auth",
"projects": "/api/v1/projects",
"project-issues-stats": "/api/v1/projects/{}/issues_stats",
}
if __name__ == "__main__":
headers = copy.deepcopy(BASE_HEADERS)
# Login
username = input("Type your username or email:\n> ")
password = getpass.getpass("Type your password:\n> ")
url = urljoin(API_HOST, URLS["auth"])
data = {
"username": username,
"password": password,
"type": "normal"
}
response = requests.post(url, json=data, headers=headers)
me = response.json()
headers["Authorization"] = "Bearer {}".format(me["auth_token"])
# List all my projects
url = urljoin(API_HOST, URLS["projects"])
params = {
"member": me["id"]
}
response = requests.get(url, params=params, headers=headers)
projects = response.json()
project_list_display = "\n".join(
[" {} - {}".format(i, p["name"]) for i, p in enumerate(projects)]
)
index = int(input("Select project: \n{}\n> ".format(project_list_display)))
project = projects[index]
# Get issues stats data
url = urljoin(API_HOST, URLS["project-issues-stats"].format(project["id"]))
response = requests.get(url, headers=headers)
issues_stats = response.json()
# GRAPHS
style = pygal.style.NeonStyle()
style.background = "transparent"
# Draw graph: Issues by status
chart = pygal.Pie(show_legend=False, style=style)
chart.title = "Issues by Status in {}".format(project["name"])
for item in issues_stats["issues_per_status"].values():
chart.add(item["name"], [{
"value": item["count"],
"color": item["color"]
}])
chart.render_to_file("../../output/chart_issues_by_status.svg")
print("Generate graph: 'Issues by Status in {}'".format(project["name"]))
# Draw graph: Issues per Assigned to
chart = pygal.HorizontalBar(style=style)
chart.title = "Issues by Assignation in {}".format(project["name"])
for item in issues_stats["issues_per_assigned_to"].values():
if item["count"] > 0:
chart.add(item["name"], item["count"])
chart.render_to_file("../../output/chart_issues_by_assigned_to.svg")
print("Generate graph: 'Issues by Assignation in {}'".format(project["name"]))
# Draw graph: Issues open/closed last month
today = datetime.today()
last_four_weeks = [today - timedelta(days=x) for x in range(28, 0, -1)]
chart = pygal.Dot(x_label_rotation=30, style=style)
chart.title = "Issues Open/Closed last 4 weeks in {}".format(project["name"])
chart.x_labels = [d.strftime("%Y %b %d") for d in last_four_weeks]
chart.add("Open",
issues_stats["last_four_weeks_days"]["by_open_closed"]["open"])
chart.add("Closed",
issues_stats["last_four_weeks_days"]["by_open_closed"]["closed"])
chart.render_to_file("../../output/chart_issues_open_close_last_4_weeks.svg")
print("Generate graph: 'Issues Open/Closed last 4 weeks in {}'".format(
project["name"]))
A Python module for communicating with the Taiga API
#!/usr/bin/env python
import getpass
from datetime import datetime, timedelta
from taiga import TaigaAPI
import pygal
API_HOST = "https://api.taiga.io/"
if __name__ == "__main__":
api = TaigaAPI(host=API_HOST)
# Login
username = input("Type your username or email:\n> ")
password = getpass.getpass("Type your password:\n> ")
api.auth(username=username, password=password)
me = api.me()
# List all my projects
projects = api.projects.list(member=me.id)
project_list_display = "\n".join(
[" {} - {}".format(i, p.name) for i, p in enumerate(projects)]
)
index = int(input("Select project: \n{}\n> ".format(project_list_display)))
project = projects[index]
# Get issues stats data
issues_stats = project.issues_stats()
# GRAPHS
style = pygal.style.NeonStyle()
style.background = "transparent"
# Draw graph: Issues by status
chart = pygal.Pie(show_legend=False, style=style)
chart.title = "Issues by Status in {}".format(project.name)
for item in issues_stats["issues_per_status"].values():
chart.add(item["name"], [{
"value": item["count"],
"color": item["color"]
}])
chart.render_to_file("../../output/chart_issues_by_status.v2.svg")
print("Generate graph: 'Issues by Status in {}'".format(project.name))
# Draw graph: Issues per Assigned to
chart = pygal.HorizontalBar(style=style)
chart.title = "Issues by Assignation in {}".format(project.name)
for item in issues_stats["issues_per_assigned_to"].values():
if item["count"] > 0:
chart.add(item["name"], item["count"])
chart.render_to_file("../../output/chart_issues_by_assigned_to.v2.svg")
print("Generate graph: 'Issues by Assignation in {}'".format(project.name))
# Draw graph: Issues open/closed last month
today = datetime.today()
last_four_weeks = [today - timedelta(days=x) for x in range(28, 0, -1)]
chart = pygal.Dot(x_label_rotation=30, style=style)
chart.title = "Issues Open/Closed last 4 weeks in {}".format(project.name)
chart.x_labels = [d.strftime("%Y %b %d") for d in last_four_weeks]
chart.add("Open",
issues_stats["last_four_weeks_days"]["by_open_closed"]["open"])
chart.add("Closed",
issues_stats["last_four_weeks_days"]["by_open_closed"]["closed"])
chart.render_to_file(
"../../output/chart_issues_open_close_last_4_weeks.v2.svg")
print("Generate graph: 'Issues Open/Closed last 4 weeks in {}'".format(
project.name))
#!/usr/bin/env python
from taiga import TaigaAPI
import click
API_HOST = "https://api.taiga.io/"
api = TaigaAPI(host=API_HOST)
@click.group()
@click.option("--username", "-u", prompt=True, help="taiga.io useername or email")
@click.option("--password", "-p", prompt=True, hide_input=True,
help="taiga.io password")
def cli(username, password):
"""The taiga command interface."""
api.auth(username=username, password=password)
@cli.command()
@click.option("--project", "-p", "project_slug", prompt=True, help="project slug")
@click.option("--subject", "-s", prompt=True, help="issue subject")
@click.option("--description", "-d", help="issue description")
@click.option("--attachments", "-a", multiple=True, help="issue description")
def add_issue(project_slug, subject, description, attachments):
"""Create a new issue."""
click.echo(" Getting project '{}'...".format(project_slug))
project = api.projects.list().get(slug=project_slug)
click.echo(" Creatting issue...'{}'".format(subject))
description = description or click.edit("...write description here..") or ""
issue = project.add_issue(subject,
project.default_priority,
project.default_issue_status,
project.default_issue_type,
project.default_severity,
description=description)
if attachments:
with click.progressbar(attachments,
label=" Attaching files...") as progressbar:
for att in progressbar:
issue.attach(att)
if __name__ == "__main__":
cli()
./taiga-ci
Usage: taiga-ci [OPTIONS] COMMAND [ARGS]... The taiga command interface. Options: -u, --username TEXT taiga.io useername or email -p, --password TEXT taiga.io password --help Show this message and exit. Commands: add_issue Create a new issue.
./taiga-ci add_issue --help
Username: bameda Password: Usage: taiga-ci add_issue [OPTIONS] Create a new issue. Options: -p, --project TEXT project slug -s, --subject TEXT issue subject -d, --description TEXT issue description -a, --attachments TEXT issue description --help Show this message and exit.
./taiga-ci add_issue -p sandbox \
-s "Test add issue cmd" \
-a ~/sample1.txt \
-a ~/sample2.txt
Username: bameda Password: Getting project 'sandbox'... Creatting issue...'Test add issue cmd' Attaching files... [####################################] 100%
Taiga curses client for terminal lovers ;-)
Creates issues in Taiga from Sentry exceptions.
Yes... creating plugins...
Version 1.0
2015-12-04
© 2015 David Barragán Merino
This work is licensed under a Creative Commons Attribution 4.0 International License.
Based on a work at https://github.com/bameda/slides/tree/master/2015/pycones_2015/hacking_the_taiga.
by David Barragán Merino / @bameda