Added documentation for REST API layer.

The goal of this patch is to setup infrastructure and organization
of docs in the project.

Changes:
* Added documentation for REST objects along with examples
* Added documentation for REST endpoints
* Added auto-generation of REST API docs

Note(1):
New API endpoint will not be auto-added to the docs. It is responibility
of committer to add new endpoint to the docs.

Note(2):
Usage examples still need to be added manually.

Change-Id: I080a6daba2fdb8a1a6a18087c7b3929da5b4bf1a
This commit is contained in:
Ruslan Kamaldinov 2014-01-26 21:19:17 +04:00
parent 5a0143a698
commit fff51b88cc
10 changed files with 321 additions and 0 deletions

View File

@ -23,9 +23,15 @@ sys.path.insert(0, os.path.abspath('../..'))
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinxcontrib.httpdomain',
'sphinxcontrib.pecanwsme.rest',
'wsmeext.sphinxext',
'oslo.sphinx'
]
wsme_protocols = ['restjson', 'restxml']
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
# execute "export SPHINX_DEBUG=1" in your terminal to disable

View File

@ -36,6 +36,14 @@ Table of contents
contributing
Client API Reference
--------------------
.. toctree::
:maxdepth: 1
webapi/v1
Indices and tables
==================

87
doc/source/webapi/v1.rst Normal file
View File

@ -0,0 +1,87 @@
============
V1 Web API
============
###
API
###
Projects
========
.. rest-controller:: storyboard.api.v1.projects:ProjectsController
:webprefix: /v1/projects
Project Groups
==============
.. rest-controller:: storyboard.api.v1.project_groups:ProjectGroupsController
:webprefix: /v1/projects
Stories
=======
.. rest-controller:: storyboard.api.v1.stories:StoriesController
:webprefix: /v1/projects
Tasks
=====
.. rest-controller:: storyboard.api.v1.tasks:TasksController
:webprefix: /v1/projects
Teams
=====
.. rest-controller:: storyboard.api.v1.teams:TeamsController
:webprefix: /v1/projects
Users
=====
.. rest-controller:: storyboard.api.v1.users:UsersController
:webprefix: /v1/projects
############
Object model
############
Comment
=======
.. autotype:: storyboard.api.v1.wsme_models.Comment
:members:
Permission
==========
.. autotype:: storyboard.api.v1.wsme_models.Permission
:members:
Project
=======
.. autotype:: storyboard.api.v1.wsme_models.Project
:members:
ProjectGroup
============
.. autotype:: storyboard.api.v1.wsme_models.ProjectGroup
:members:
Story
=====
.. autotype:: storyboard.api.v1.wsme_models.Story
:members:
StoryTag
========
.. autotype:: storyboard.api.v1.wsme_models.StoryTag
:members:
Task
====
.. autotype:: storyboard.api.v1.wsme_models.Task
:members:
Team
====
.. autotype:: storyboard.api.v1.wsme_models.Team
:members:
User
====
.. autotype:: storyboard.api.v1.wsme_models.User
:members:

View File

@ -21,9 +21,17 @@ import storyboard.api.v1.wsme_models as wsme_models
class ProjectGroupsController(rest.RestController):
"""REST controller for Project Groups.
At this moment it provides read-only operations.
"""
@wsme_pecan.wsexpose(wsme_models.ProjectGroup, int)
def get_one(self, id):
"""Retrieve information about the given project group.
:param name: project group name.
"""
group = wsme_models.ProjectGroup.get(id=id)
if not group:
raise ClientSideError("Project Group %s not found" % id,
@ -32,12 +40,17 @@ class ProjectGroupsController(rest.RestController):
@wsme_pecan.wsexpose([wsme_models.ProjectGroup])
def get(self):
"""Retrieve a list of projects groups."""
groups = wsme_models.ProjectGroup.get_all()
return groups
@wsme_pecan.wsexpose(wsme_models.ProjectGroup,
body=wsme_models.ProjectGroup)
def post(self, group):
"""Create a new project group.
:param group: a project group within the request body.
"""
created_group = wsme_models.ProjectGroup.create(wsme_entry=group)
if not created_group:
raise ClientSideError("Could not create ProjectGroup")
@ -46,6 +59,11 @@ class ProjectGroupsController(rest.RestController):
@wsme_pecan.wsexpose(wsme_models.ProjectGroup, int,
body=wsme_models.ProjectGroup)
def put(self, id, group):
"""Modify this project group.
:param id: An ID of the project group.
:param group: a project group within the request body.
"""
updated_group = wsme_models.ProjectGroup.update("id", id, group)
if not updated_group:
raise ClientSideError("Could not update group %s" % id)

View File

@ -21,9 +21,17 @@ import storyboard.api.v1.wsme_models as wsme_models
class ProjectsController(rest.RestController):
"""REST controller for Projects.
At this moment it provides read-only operations.
"""
@wsme_pecan.wsexpose(wsme_models.Project, unicode)
def get_one(self, name):
"""Retrieve information about the given project.
:param name: project name.
"""
project = wsme_models.Project.get(name=name)
if not project:
raise ClientSideError("Project %s not found" % name,
@ -32,5 +40,7 @@ class ProjectsController(rest.RestController):
@wsme_pecan.wsexpose([wsme_models.Project])
def get(self):
"""Retrieve a list of projects.
"""
projects = wsme_models.Project.get_all()
return projects

View File

@ -21,6 +21,7 @@ import storyboard.api.v1.wsme_models as wsme_models
class StoriesController(rest.RestController):
"""Manages operations on stories."""
_custom_actions = {
"add_task": ["POST"],
@ -29,6 +30,10 @@ class StoriesController(rest.RestController):
@wsme_pecan.wsexpose(wsme_models.Story, unicode)
def get_one(self, id):
"""Retrieve details about one story.
:param id: An ID of the story.
"""
story = wsme_models.Story.get(id=id)
if not story:
raise ClientSideError("Story %s not found" % id,
@ -37,11 +42,16 @@ class StoriesController(rest.RestController):
@wsme_pecan.wsexpose([wsme_models.Story])
def get(self):
"""Retrieve definitions of all of the stories."""
stories = wsme_models.Story.get_all()
return stories
@wsme_pecan.wsexpose(wsme_models.Story, wsme_models.Story)
def post(self, story):
"""Create a new story.
:param story: a story within the request body.
"""
created_story = wsme_models.Story.create(wsme_entry=story)
if not created_story:
raise ClientSideError("Could not create a story")
@ -49,6 +59,11 @@ class StoriesController(rest.RestController):
@wsme_pecan.wsexpose(wsme_models.Story, unicode, wsme_models.Story)
def put(self, story_id, story):
"""Modify this story.
:param story_id: An ID of the story.
:param story: a story within the request body.
"""
updated_story = wsme_models.Story.update("id", story_id, story)
if not updated_story:
raise ClientSideError("Could not update story %s" % story_id)
@ -56,6 +71,11 @@ class StoriesController(rest.RestController):
@wsme_pecan.wsexpose(wsme_models.Story, unicode, wsme_models.Task)
def add_task(self, story_id, task):
"""Associate a task with a story.
:param story_id: An ID of the story.
:param task: a task within the request body.
"""
updated_story = wsme_models.Story.add_task(story_id, task)
if not updated_story:
raise ClientSideError("Could not add task to story %s" % story_id)
@ -63,6 +83,11 @@ class StoriesController(rest.RestController):
@wsme_pecan.wsexpose(wsme_models.Story, unicode, wsme_models.Comment)
def add_comment(self, story_id, comment):
"""Add a comment with a story.
:param story_id: An ID of the story.
:param comment: a comment within the request body.
"""
updated_story = wsme_models.Story.add_comment(story_id, comment)
if not updated_story:
raise ClientSideError("Could not add comment to story %s"

View File

@ -21,9 +21,14 @@ import storyboard.api.v1.wsme_models as wsme_models
class TasksController(rest.RestController):
"""Manages tasks."""
@wsme_pecan.wsexpose(wsme_models.Task, unicode)
def get_one(self, id):
"""Retrieve details about one task.
:param id: An ID of the task.
"""
task = wsme_models.Task.get(id=id)
if not task:
raise ClientSideError("Task %s not found" % id,
@ -32,11 +37,17 @@ class TasksController(rest.RestController):
@wsme_pecan.wsexpose([wsme_models.Task])
def get(self):
"""Retrieve definitions of all of the tasks."""
tasks = wsme_models.Task.get_all()
return tasks
@wsme_pecan.wsexpose(wsme_models.Task, unicode, wsme_models.Task)
def put(self, task_id, task):
"""Modify this task.
:param task_id: An ID of the task.
:param task: a task within the request body.
"""
updated_task = wsme_models.Task.update("id", task_id, task)
if not updated_task:
raise ClientSideError("Could not update story %s" % task_id)

View File

@ -22,6 +22,7 @@ import storyboard.api.v1.wsme_models as wsme_models
class TeamsController(rest.RestController):
"""Manages teams."""
_custom_actions = {
"add_user": ["POST"]
@ -29,6 +30,10 @@ class TeamsController(rest.RestController):
@wsme_pecan.wsexpose(wsme_models.Team, unicode)
def get_one(self, name):
"""Retrieve details about one team.
:param name: unique name to identify the team.
"""
team = wsme_models.Team.get(name=name)
if not team:
raise ClientSideError("Team %s not found" % name,
@ -37,11 +42,16 @@ class TeamsController(rest.RestController):
@wsme_pecan.wsexpose([wsme_models.Team])
def get(self):
"""Retrieve definitions of all of the teams."""
teams = wsme_models.Team.get_all()
return teams
@wsme_pecan.wsexpose(wsme_models.Team, wsme_models.Team)
def post(self, team):
"""Create a new team.
:param team: a team within the request body.
"""
created_team = wsme_models.Team.create(wsme_entry=team)
if not created_team:
raise ClientSideError("Could not create a team")
@ -49,6 +59,11 @@ class TeamsController(rest.RestController):
@wsme_pecan.wsexpose(wsme_models.Team, unicode, unicode)
def add_user(self, team_name, username):
"""Associate a user with the team.
:param team_name: unique name to identify the team.
:param username: unique name to identify the user.
"""
updated_team = wsme_models.Team.add_user(team_name, username)
if not updated_team:
raise ClientSideError("Could not add user %s to team %s"

View File

@ -21,14 +21,20 @@ import storyboard.api.v1.wsme_models as wsme_models
class UsersController(rest.RestController):
"""Manages users."""
@wsme_pecan.wsexpose([wsme_models.User])
def get(self):
"""Retrieve definitions of all of the users."""
users = wsme_models.User.get_all()
return users
@wsme_pecan.wsexpose(wsme_models.User, unicode)
def get_one(self, username):
"""Retrieve details about one user.
:param username: unique name to identify the user.
"""
user = wsme_models.User.get(username=username)
if not user:
raise ClientSideError("User %s not found" % username,
@ -37,6 +43,10 @@ class UsersController(rest.RestController):
@wsme_pecan.wsexpose(wsme_models.User, wsme_models.User)
def post(self, user):
"""Create a new user.
:param user: a user within the request body.
"""
created_user = wsme_models.User.create(wsme_entry=user)
if not created_user:
raise ClientSideError("Could not create User")
@ -44,6 +54,11 @@ class UsersController(rest.RestController):
@wsme_pecan.wsexpose(wsme_models.User, unicode, wsme_models.User)
def put(self, username, user):
"""Modify this user.
:param username: unique name to identify the user.
:param user: a user within the request body.
"""
updated_user = wsme_models.User.update("username", username, user)
if not updated_user:
raise ClientSideError("Could not update user %s" % username)

View File

@ -191,46 +191,121 @@ def update_db_model(cls, db_entry, wsme_entry):
class Project(_Base):
"""The Storyboard Registry describes the open source world as ProjectGroups
and Products. Each ProjectGroup may be responsible for several Projects.
For example, the OpenStack Infrastructure Project has Zuul, Nodepool,
Storyboard as Projects, among others.
"""
name = wtypes.text
"""At least one lowercase letter or number, followed by letters, numbers,
dots, hyphens or pluses. Keep this name short; it is used in URLs.
"""
description = wtypes.text
"""Details about the project's work, highlights, goals, and how to
contribute. Use plain text, paragraphs are preserved and URLs are
linked in pages.
"""
@classmethod
def sample(cls):
return cls(
name="Storyboard",
description="Awesome project")
class ProjectGroup(_Base):
"""Represents a group of projects."""
name = wtypes.text
"""A unique name, used in URLs, identifying the project group. All
lowercase, no special characters. Examples: infra, compute.
"""
title = wtypes.text
"""The full name of the project group, which can contain spaces, special
characters, etc.
"""
@classmethod
def sample(cls):
return cls(
name="Infra",
title="Awesome project")
class Permission(_Base):
"""Permissions can be associated with users and teams."""
pass
class Task(_Base):
"""Represents a task within a story."""
pass
class StoryTag(_Base):
"""Tags are used classifying user-stories."""
pass
# TODO(ruhe): clarify and document what are 'action' and 'type' for
class Comment(_Base):
"""Represents a comment."""
#todo(nkonovalov): replace with a enum
action = wtypes.text
"""Comment action. Allowed values: unknown."""
comment_type = wtypes.text
"""Comment type. Allowed values: unknown."""
content = wtypes.text
"""All the text/plain chunks joined together as a unicode string."""
story_id = int
"""ID of corresponding user-story."""
author_id = int
"""Comment author ID."""
@classmethod
def sample(cls):
return cls(
action="action",
comment_type="type1",
content="comment content goes here",
story_id=42,
author_id=67)
class Story(_Base):
"""Represents a user-story."""
title = wtypes.text
"""A descriptive label for this tracker to show in listings."""
description = wtypes.text
"""A brief introduction or overview of this bug tracker instance."""
is_bug = bool
"""Is this a bug or a feature :)"""
#todo(nkonovalov): replace with a enum
priority = wtypes.text
"""Priority.
Allowed values: ['Undefined', 'Low', 'Medium', 'High', 'Critical'].
"""
tasks = wtypes.ArrayType(Task)
"""List of linked tasks."""
comments = wtypes.ArrayType(Comment)
"""List of linked comments."""
tags = wtypes.ArrayType(StoryTag)
"""List of linked tags."""
@classmethod
def add_task(cls, story_id, task):
@ -241,25 +316,76 @@ class Story(_Base):
return cls.create_and_add_item("id", story_id, Comment, comment,
"comments")
@classmethod
def sample(cls):
return cls(
title="Use Storyboard to manage Storyboard",
description="We should use Storyboard to manage Storyboard",
is_bug=False,
priority='Critical',
tasks=[],
comments=[],
tags=[])
class User(_Base):
"""Represents a user."""
username = wtypes.text
"""A short unique name, beginning with a lower-case letter or number, and
containing only letters, numbers, dots, hyphens, or plus signs"""
first_name = wtypes.text
"""First name."""
last_name = wtypes.text
"""Last name."""
email = wtypes.text
"""Email Address."""
# TODO(ruhe): clarify and document what are these fields for
is_staff = bool
is_active = bool
is_superuser = bool
last_login = datetime
"""Date of the last login."""
#teams = wtypes.ArrayType(Team)
permissions = wtypes.ArrayType(Permission)
"""List of associated permissions"""
#tasks = wtypes.ArrayType(Task)
@classmethod
def sample(cls):
return cls(
username="elbarto",
first_name="Bart",
last_name="Simpson",
email="skinnerstinks@springfield.net",
is_staff=False,
is_active=True,
is_superuser=True,
last_login=datetime(2014, 1, 1, 16, 42),
permissions=[])
class Team(_Base):
"""A group of people and other teams."""
name = wtypes.text
"""A short unique name, beginning with a lower-case letter or number,
and containing only letters, numbers, dots, hyphens, or plus signs.
"""
users = wtypes.ArrayType(User)
"""List of direct members."""
permissions = wtypes.ArrayType(Permission)
"""Collection of associated permissions."""
@classmethod
def add_user(cls, team_name, username):