From fff51b88cc4e3e0e707716c3d001844753487dc2 Mon Sep 17 00:00:00 2001 From: Ruslan Kamaldinov Date: Sun, 26 Jan 2014 21:19:17 +0400 Subject: [PATCH] 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 --- doc/source/conf.py | 6 ++ doc/source/index.rst | 8 ++ doc/source/webapi/v1.rst | 87 +++++++++++++++++++ storyboard/api/v1/project_groups.py | 18 ++++ storyboard/api/v1/projects.py | 10 +++ storyboard/api/v1/stories.py | 25 ++++++ storyboard/api/v1/tasks.py | 11 +++ storyboard/api/v1/teams.py | 15 ++++ storyboard/api/v1/users.py | 15 ++++ storyboard/api/v1/wsme_models.py | 126 ++++++++++++++++++++++++++++ 10 files changed, 321 insertions(+) create mode 100644 doc/source/webapi/v1.rst diff --git a/doc/source/conf.py b/doc/source/conf.py index 3aae9545..7e5ca83c 100755 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -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 diff --git a/doc/source/index.rst b/doc/source/index.rst index be6a1c63..c3c38243 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -36,6 +36,14 @@ Table of contents contributing +Client API Reference +-------------------- + +.. toctree:: + :maxdepth: 1 + + webapi/v1 + Indices and tables ================== diff --git a/doc/source/webapi/v1.rst b/doc/source/webapi/v1.rst new file mode 100644 index 00000000..4eb793b3 --- /dev/null +++ b/doc/source/webapi/v1.rst @@ -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: diff --git a/storyboard/api/v1/project_groups.py b/storyboard/api/v1/project_groups.py index 75f137ad..ed0ea20a 100644 --- a/storyboard/api/v1/project_groups.py +++ b/storyboard/api/v1/project_groups.py @@ -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) diff --git a/storyboard/api/v1/projects.py b/storyboard/api/v1/projects.py index ccea2654..2d647a82 100644 --- a/storyboard/api/v1/projects.py +++ b/storyboard/api/v1/projects.py @@ -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 diff --git a/storyboard/api/v1/stories.py b/storyboard/api/v1/stories.py index 67d2e4b4..68bca4da 100644 --- a/storyboard/api/v1/stories.py +++ b/storyboard/api/v1/stories.py @@ -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" diff --git a/storyboard/api/v1/tasks.py b/storyboard/api/v1/tasks.py index 326f3952..4e55a563 100644 --- a/storyboard/api/v1/tasks.py +++ b/storyboard/api/v1/tasks.py @@ -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) diff --git a/storyboard/api/v1/teams.py b/storyboard/api/v1/teams.py index 62294755..d5f036d8 100644 --- a/storyboard/api/v1/teams.py +++ b/storyboard/api/v1/teams.py @@ -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" diff --git a/storyboard/api/v1/users.py b/storyboard/api/v1/users.py index eeda4be8..eaaf20ab 100644 --- a/storyboard/api/v1/users.py +++ b/storyboard/api/v1/users.py @@ -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) diff --git a/storyboard/api/v1/wsme_models.py b/storyboard/api/v1/wsme_models.py index 10cb7089..ce855671 100644 --- a/storyboard/api/v1/wsme_models.py +++ b/storyboard/api/v1/wsme_models.py @@ -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):