Introducing basic REST API
This is initial commit adding pecan/wsme framework. Example operations are: * GET /v1/project_groups * GET /v1/project_groups/<group_name> * GET /v1/projects * GET /v1/projects/<project_name> * GET /v1/teams * GET /v1/teams/<team_name> * POST /v1/teams * POST /v1/teams/add_user * GET /v1/users * GET /v1/users/<username> * POST /v1/users * PUT /v1/users/<username> * GET /v1/stories * GET /v1/stories/<story_id> * POST /v1/stories * PUT /v1/stories * POST /v1/stories/add_task * POST /v1/stories/add_comment * GET /v1/tasks * GET /v1/tasks/<task_id> * PUT /v1/tasks More detailed documentation will be added later to a wiki page. Tests will be added in a separate CR. Auth stuff will be added in a separate CR after it is dicussed. Change-Id: Ibace8cf7dd5bb933b0d2484b1d57b79bb8441a28
This commit is contained in:
parent
1ce4b9a9f5
commit
b02a396f3b
@ -1,5 +1,6 @@
|
|||||||
include AUTHORS
|
include AUTHORS
|
||||||
include ChangeLog
|
include ChangeLog
|
||||||
|
include storyboard/api/app.wsgi
|
||||||
include storyboard/db/migration/README
|
include storyboard/db/migration/README
|
||||||
include storyboard/db/migration/alembic.ini
|
include storyboard/db/migration/alembic.ini
|
||||||
include storyboard/db/migration/alembic_migrations/script.py.mako
|
include storyboard/db/migration/alembic_migrations/script.py.mako
|
||||||
|
@ -39,7 +39,7 @@ lock_path = $state_path/lock
|
|||||||
# connection = mysql://root:pass@127.0.0.1:3306/storyboard
|
# connection = mysql://root:pass@127.0.0.1:3306/storyboard
|
||||||
# Replace 127.0.0.1 above with the IP address of the database used by the
|
# Replace 127.0.0.1 above with the IP address of the database used by the
|
||||||
# main storyboard server. (Leave it as is if the database runs on this host.)
|
# main storyboard server. (Leave it as is if the database runs on this host.)
|
||||||
# connection = sqlite://
|
# connection=sqlite://
|
||||||
|
|
||||||
# The SQLAlchemy connection string used to connect to the slave database
|
# The SQLAlchemy connection string used to connect to the slave database
|
||||||
# slave_connection =
|
# slave_connection =
|
||||||
@ -72,3 +72,9 @@ lock_path = $state_path/lock
|
|||||||
|
|
||||||
# If set, use this value for pool_timeout with sqlalchemy
|
# If set, use this value for pool_timeout with sqlalchemy
|
||||||
# pool_timeout = 10
|
# pool_timeout = 10
|
||||||
|
|
||||||
|
[api]
|
||||||
|
#host="0.0.0.0"
|
||||||
|
#
|
||||||
|
#port=8080
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
module=db
|
module=db
|
||||||
module=db.sqlalchemy
|
module=db.sqlalchemy
|
||||||
module=processutils
|
module=processutils
|
||||||
|
module=log
|
||||||
|
|
||||||
# The base module to hold the copy of openstack.common
|
# The base module to hold the copy of openstack.common
|
||||||
base=storyboard
|
base=storyboard
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
pbr>=0.5.21,<1.0
|
pbr>=0.5.21,<1.0
|
||||||
|
|
||||||
|
alembic>=0.4.1
|
||||||
|
Babel>=0.9.6
|
||||||
Django>=1.4,<1.6
|
Django>=1.4,<1.6
|
||||||
django-openid-auth
|
django-openid-auth
|
||||||
|
iso8601>=0.1.8
|
||||||
markdown
|
markdown
|
||||||
|
oslo.config>=1.2.0
|
||||||
|
pecan>=0.2.0
|
||||||
python-openid
|
python-openid
|
||||||
six>=1.4.1
|
six>=1.4.1
|
||||||
Babel>=0.9.6
|
|
||||||
SQLAlchemy>=0.8,<=0.8.99
|
SQLAlchemy>=0.8,<=0.8.99
|
||||||
alembic>=0.4.1
|
WSME>=0.5b6
|
||||||
oslo.config>=1.2.0
|
|
||||||
iso8601>=0.1.8
|
|
||||||
|
@ -31,6 +31,7 @@ data_files =
|
|||||||
|
|
||||||
[entry_points]
|
[entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
|
storyboard-api = storyboard.api.app:start
|
||||||
storyboard-db-manage = storyboard.db.migration.cli:main
|
storyboard-db-manage = storyboard.db.migration.cli:main
|
||||||
|
|
||||||
[build_sphinx]
|
[build_sphinx]
|
||||||
|
0
storyboard/api/__init__.py
Normal file
0
storyboard/api/__init__.py
Normal file
86
storyboard/api/app.py
Normal file
86
storyboard/api/app.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
import pecan
|
||||||
|
from storyboard.openstack.common.gettextutils import _ # noqa
|
||||||
|
from storyboard.openstack.common import log
|
||||||
|
from wsgiref import simple_server
|
||||||
|
|
||||||
|
from storyboard.api import config as api_config
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
API_OPTS = [
|
||||||
|
cfg.StrOpt('host',
|
||||||
|
default='0.0.0.0',
|
||||||
|
help='API host'),
|
||||||
|
cfg.IntOpt('port',
|
||||||
|
default=8080,
|
||||||
|
help='API port')
|
||||||
|
]
|
||||||
|
CONF.register_opts(API_OPTS, 'api')
|
||||||
|
|
||||||
|
|
||||||
|
def get_pecan_config():
|
||||||
|
# Set up the pecan configuration
|
||||||
|
filename = api_config.__file__.replace('.pyc', '.py')
|
||||||
|
return pecan.configuration.conf_from_file(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_app(pecan_config=None):
|
||||||
|
if not pecan_config:
|
||||||
|
pecan_config = get_pecan_config()
|
||||||
|
|
||||||
|
app = pecan.make_app(
|
||||||
|
pecan_config.app.root,
|
||||||
|
debug=CONF.debug,
|
||||||
|
force_canonical=getattr(pecan_config.app, 'force_canonical', True),
|
||||||
|
guess_content_type_from_ext=False
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg.set_defaults(log.log_opts,
|
||||||
|
default_log_levels=[
|
||||||
|
'storyboard=INFO',
|
||||||
|
'sqlalchemy=WARN'
|
||||||
|
])
|
||||||
|
log.setup('storyboard')
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
root = setup_app()
|
||||||
|
CONF(project='storyboard')
|
||||||
|
|
||||||
|
# Create the WSGI server and start it
|
||||||
|
host = cfg.CONF.api.host
|
||||||
|
port = cfg.CONF.api.port
|
||||||
|
srv = simple_server.make_server(host, port, root)
|
||||||
|
|
||||||
|
LOG.info(_('Starting server in PID %s') % os.getpid())
|
||||||
|
LOG.info(_("Configuration:"))
|
||||||
|
if host == '0.0.0.0':
|
||||||
|
LOG.info(_(
|
||||||
|
'serving on 0.0.0.0:%(port)s, view at http://127.0.0.1:%(port)s')
|
||||||
|
% ({'port': port}))
|
||||||
|
else:
|
||||||
|
LOG.info(_("serving on http://%(host)s:%(port)s") % (
|
||||||
|
{'host': host, 'port': port}))
|
||||||
|
|
||||||
|
srv.serve_forever()
|
23
storyboard/api/app.wsgi
Normal file
23
storyboard/api/app.wsgi
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from storyboard.api import app
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
CONF(project='storyboard')
|
||||||
|
|
||||||
|
application = app.setup_app()
|
20
storyboard/api/config.py
Normal file
20
storyboard/api/config.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
app = {
|
||||||
|
'root': 'storyboard.api.root_controller.RootController',
|
||||||
|
'modules': ['storyboard.api'],
|
||||||
|
'debug': False
|
||||||
|
}
|
20
storyboard/api/root_controller.py
Normal file
20
storyboard/api/root_controller.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from storyboard.api.v1.v1_controller import V1Controller
|
||||||
|
|
||||||
|
|
||||||
|
class RootController(object):
|
||||||
|
v1 = V1Controller()
|
0
storyboard/api/v1/__init__.py
Normal file
0
storyboard/api/v1/__init__.py
Normal file
36
storyboard/api/v1/project_groups.py
Normal file
36
storyboard/api/v1/project_groups.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pecan import rest
|
||||||
|
from wsme.exc import ClientSideError
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
import storyboard.api.v1.wsme_models as wsme_models
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectGroupsController(rest.RestController):
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.ProjectGroup, unicode)
|
||||||
|
def get_one(self, name):
|
||||||
|
group = wsme_models.ProjectGroup.get(name=name)
|
||||||
|
if not group:
|
||||||
|
raise ClientSideError("Project Group %s not found" % name,
|
||||||
|
status_code=404)
|
||||||
|
return group
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose([wsme_models.ProjectGroup])
|
||||||
|
def get(self):
|
||||||
|
groups = wsme_models.ProjectGroup.get_all()
|
||||||
|
return groups
|
36
storyboard/api/v1/projects.py
Normal file
36
storyboard/api/v1/projects.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pecan import rest
|
||||||
|
from wsme.exc import ClientSideError
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
import storyboard.api.v1.wsme_models as wsme_models
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectsController(rest.RestController):
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Project, unicode)
|
||||||
|
def get_one(self, name):
|
||||||
|
project = wsme_models.Project.get(name=name)
|
||||||
|
if not project:
|
||||||
|
raise ClientSideError("Project %s not found" % name,
|
||||||
|
status_code=404)
|
||||||
|
return project
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose([wsme_models.Project])
|
||||||
|
def get(self):
|
||||||
|
projects = wsme_models.Project.get_all()
|
||||||
|
return projects
|
70
storyboard/api/v1/stories.py
Normal file
70
storyboard/api/v1/stories.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pecan import rest
|
||||||
|
from wsme.exc import ClientSideError
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
import storyboard.api.v1.wsme_models as wsme_models
|
||||||
|
|
||||||
|
|
||||||
|
class StoriesController(rest.RestController):
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
"add_task": ["POST"],
|
||||||
|
"add_comment": ["POST"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Story, unicode)
|
||||||
|
def get_one(self, id):
|
||||||
|
story = wsme_models.Story.get(id=id)
|
||||||
|
if not story:
|
||||||
|
raise ClientSideError("Story %s not found" % id,
|
||||||
|
status_code=404)
|
||||||
|
return story
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose([wsme_models.Story])
|
||||||
|
def get(self):
|
||||||
|
stories = wsme_models.Story.get_all()
|
||||||
|
return stories
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Story, wsme_models.Story)
|
||||||
|
def post(self, story):
|
||||||
|
created_story = wsme_models.Story.create(wsme_entry=story)
|
||||||
|
if not created_story:
|
||||||
|
raise ClientSideError("Could not create a story")
|
||||||
|
return created_story
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Story, unicode, wsme_models.Story)
|
||||||
|
def put(self, story_id, story):
|
||||||
|
updated_story = wsme_models.Story.update("id", story_id, story)
|
||||||
|
if not updated_story:
|
||||||
|
raise ClientSideError("Could not update story %s" % story_id)
|
||||||
|
return updated_story
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Story, unicode, wsme_models.Task)
|
||||||
|
def add_task(self, story_id, task):
|
||||||
|
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)
|
||||||
|
return updated_story
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Story, unicode, wsme_models.Comment)
|
||||||
|
def add_comment(self, story_id, comment):
|
||||||
|
updated_story = wsme_models.Story.add_comment(story_id, comment)
|
||||||
|
if not updated_story:
|
||||||
|
raise ClientSideError("Could not add comment to story %s"
|
||||||
|
% story_id)
|
||||||
|
return updated_story
|
43
storyboard/api/v1/tasks.py
Normal file
43
storyboard/api/v1/tasks.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pecan import rest
|
||||||
|
from wsme.exc import ClientSideError
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
import storyboard.api.v1.wsme_models as wsme_models
|
||||||
|
|
||||||
|
|
||||||
|
class TasksController(rest.RestController):
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Task, unicode)
|
||||||
|
def get_one(self, id):
|
||||||
|
task = wsme_models.Task.get(id=id)
|
||||||
|
if not task:
|
||||||
|
raise ClientSideError("Task %s not found" % id,
|
||||||
|
status_code=404)
|
||||||
|
return task
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose([wsme_models.Task])
|
||||||
|
def get(self):
|
||||||
|
tasks = wsme_models.Task.get_all()
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Task, unicode, wsme_models.Task)
|
||||||
|
def put(self, task_id, task):
|
||||||
|
updated_task = wsme_models.Task.update("id", task_id, task)
|
||||||
|
if not updated_task:
|
||||||
|
raise ClientSideError("Could not update story %s" % task_id)
|
||||||
|
return updated_task
|
56
storyboard/api/v1/teams.py
Normal file
56
storyboard/api/v1/teams.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from pecan import rest
|
||||||
|
from wsme.exc import ClientSideError
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
import storyboard.api.v1.wsme_models as wsme_models
|
||||||
|
|
||||||
|
|
||||||
|
class TeamsController(rest.RestController):
|
||||||
|
|
||||||
|
_custom_actions = {
|
||||||
|
"add_user": ["POST"]
|
||||||
|
}
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Team, unicode)
|
||||||
|
def get_one(self, name):
|
||||||
|
team = wsme_models.Team.get(name=name)
|
||||||
|
if not team:
|
||||||
|
raise ClientSideError("Team %s not found" % name,
|
||||||
|
status_code=404)
|
||||||
|
return team
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose([wsme_models.Team])
|
||||||
|
def get(self):
|
||||||
|
teams = wsme_models.Team.get_all()
|
||||||
|
return teams
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Team, wsme_models.Team)
|
||||||
|
def post(self, team):
|
||||||
|
created_team = wsme_models.Team.create(wsme_entry=team)
|
||||||
|
if not created_team:
|
||||||
|
raise ClientSideError("Could not create a team")
|
||||||
|
return created_team
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.Team, unicode, unicode)
|
||||||
|
def add_user(self, team_name, username):
|
||||||
|
updated_team = wsme_models.Team.add_user(team_name, username)
|
||||||
|
if not updated_team:
|
||||||
|
raise ClientSideError("Could not add user %s to team %s"
|
||||||
|
% (username, team_name))
|
||||||
|
return updated_team
|
50
storyboard/api/v1/users.py
Normal file
50
storyboard/api/v1/users.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from pecan import rest
|
||||||
|
from wsme.exc import ClientSideError
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
import storyboard.api.v1.wsme_models as wsme_models
|
||||||
|
|
||||||
|
|
||||||
|
class UsersController(rest.RestController):
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose([wsme_models.User])
|
||||||
|
def get(self):
|
||||||
|
users = wsme_models.User.get_all()
|
||||||
|
return users
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.User, unicode)
|
||||||
|
def get_one(self, username):
|
||||||
|
user = wsme_models.User.get(username=username)
|
||||||
|
if not user:
|
||||||
|
raise ClientSideError("User %s not found" % username,
|
||||||
|
status_code=404)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.User, wsme_models.User)
|
||||||
|
def post(self, user):
|
||||||
|
created_user = wsme_models.User.create(wsme_entry=user)
|
||||||
|
if not created_user:
|
||||||
|
raise ClientSideError("Could not create User")
|
||||||
|
return created_user
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(wsme_models.User, unicode, wsme_models.User)
|
||||||
|
def put(self, username, user):
|
||||||
|
updated_user = wsme_models.User.update("username", username, user)
|
||||||
|
if not updated_user:
|
||||||
|
raise ClientSideError("Could not update user %s" % username)
|
||||||
|
return updated_user
|
31
storyboard/api/v1/v1_controller.py
Normal file
31
storyboard/api/v1/v1_controller.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from storyboard.api.v1.project_groups import ProjectGroupsController
|
||||||
|
from storyboard.api.v1.projects import ProjectsController
|
||||||
|
from storyboard.api.v1.stories import StoriesController
|
||||||
|
from storyboard.api.v1.tasks import TasksController
|
||||||
|
from storyboard.api.v1.teams import TeamsController
|
||||||
|
from storyboard.api.v1.users import UsersController
|
||||||
|
|
||||||
|
|
||||||
|
class V1Controller(object):
|
||||||
|
|
||||||
|
project_groups = ProjectGroupsController()
|
||||||
|
projects = ProjectsController()
|
||||||
|
teams = TeamsController()
|
||||||
|
users = UsersController()
|
||||||
|
stories = StoriesController()
|
||||||
|
tasks = TasksController()
|
287
storyboard/api/v1/wsme_models.py
Normal file
287
storyboard/api/v1/wsme_models.py
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
# Copyright (c) 2013 Mirantis Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import six
|
||||||
|
import warnings
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
from sqlalchemy.exc import SADeprecationWarning
|
||||||
|
import storyboard.db.models as sqlalchemy_models
|
||||||
|
from storyboard.openstack.common.db.sqlalchemy import session as db_session
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class _Base(wtypes.Base):
|
||||||
|
|
||||||
|
id = int
|
||||||
|
created_at = datetime
|
||||||
|
updated_at = datetime
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for key, val in six.iteritems(kwargs):
|
||||||
|
setattr(self, key, val)
|
||||||
|
super(_Base, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, **kwargs):
|
||||||
|
query = cls.from_db(**kwargs)
|
||||||
|
entry = query.first()
|
||||||
|
|
||||||
|
return convert_to_wsme(cls, entry)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all(cls, **kwargs):
|
||||||
|
query = cls.from_db(**kwargs)
|
||||||
|
entries = query.all()
|
||||||
|
|
||||||
|
return [convert_to_wsme(cls, entry) for entry in entries]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, session=None, wsme_entry=None):
|
||||||
|
if not session:
|
||||||
|
session = db_session.get_session(sqlite_fk=True)
|
||||||
|
with session.begin():
|
||||||
|
db_entry = convert_to_db_model(cls, wsme_entry, session)
|
||||||
|
session.add(db_entry)
|
||||||
|
|
||||||
|
return cls.get(id=db_entry.id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update(cls, key_property_name="id", key_property_value=None,
|
||||||
|
wsme_entry=None):
|
||||||
|
db_entry = cls.from_db(**{key_property_name: key_property_value})\
|
||||||
|
.first()
|
||||||
|
if not db_entry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
session = db_session.get_session(sqlite_fk=True)
|
||||||
|
with session.begin():
|
||||||
|
updated_db_model = update_db_model(cls, db_entry, wsme_entry)
|
||||||
|
session.add(updated_db_model)
|
||||||
|
|
||||||
|
return cls.get(id=db_entry.id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_item(cls, cont_key_name, cont_key_value, item_cls, item_key_name,
|
||||||
|
item_key_value, container_name):
|
||||||
|
session = db_session.get_session(sqlite_fk=True)
|
||||||
|
with session.begin():
|
||||||
|
db_container_enty = cls.from_db(session=session,
|
||||||
|
**{cont_key_name: cont_key_value})\
|
||||||
|
.first()
|
||||||
|
if not db_container_enty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
db_add_item = item_cls.from_db(session=session,
|
||||||
|
**{item_key_name: item_key_value}).\
|
||||||
|
first()
|
||||||
|
if not db_add_item:
|
||||||
|
return None
|
||||||
|
|
||||||
|
getattr(db_container_enty, container_name).append(db_add_item)
|
||||||
|
session.add(db_container_enty)
|
||||||
|
|
||||||
|
return cls.get(**{cont_key_name: cont_key_value})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_and_add_item(cls, cont_key_name, cont_key_value, item_cls,
|
||||||
|
item_value, container_name):
|
||||||
|
|
||||||
|
wsme_item = item_cls.create(wsme_entry=item_value)
|
||||||
|
if not wsme_item:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return cls.add_item(cont_key_name, cont_key_value, item_cls, "id",
|
||||||
|
wsme_item.id, container_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_db(cls, session=None, **kwargs):
|
||||||
|
model_cls = WSME_TO_SQLALCHEMY[cls]
|
||||||
|
if not session:
|
||||||
|
session = db_session.get_session(sqlite_fk=True)
|
||||||
|
query = session.query(model_cls)
|
||||||
|
|
||||||
|
return query.filter_by(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
warnings.simplefilter("ignore", SADeprecationWarning)
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_wsme(cls, entry):
|
||||||
|
if not entry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
wsme_object = cls()
|
||||||
|
for attr in cls._wsme_attributes:
|
||||||
|
attr_name = attr.name
|
||||||
|
value = getattr(entry, attr_name)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(attr._get_datatype(), _Base):
|
||||||
|
value = convert_to_wsme(SQLALCHEMY_TO_WSME[type(attr)], value)
|
||||||
|
|
||||||
|
if isinstance(attr._get_datatype(), wtypes.ArrayType):
|
||||||
|
value = [convert_to_wsme(SQLALCHEMY_TO_WSME[type(item)], item)
|
||||||
|
for item in value]
|
||||||
|
setattr(wsme_object, attr_name, value)
|
||||||
|
|
||||||
|
return wsme_object
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_db_model(cls, entry, session):
|
||||||
|
if not entry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
model_cls = WSME_TO_SQLALCHEMY[cls]
|
||||||
|
|
||||||
|
model_object = model_cls()
|
||||||
|
for attr in cls._wsme_attributes:
|
||||||
|
attr_name = attr.name
|
||||||
|
value = getattr(entry, attr_name)
|
||||||
|
|
||||||
|
if value is None or isinstance(value, wtypes.UnsetType):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(attr._get_datatype(), _Base):
|
||||||
|
value = convert_to_db_model(type(attr), value, session)
|
||||||
|
session.add(value)
|
||||||
|
|
||||||
|
if isinstance(attr._get_datatype(), wtypes.ArrayType):
|
||||||
|
value = [convert_to_db_model(attr._get_datatype().item_type,
|
||||||
|
item,
|
||||||
|
session)
|
||||||
|
for item in value]
|
||||||
|
setattr(model_object, attr_name, value)
|
||||||
|
|
||||||
|
return model_object
|
||||||
|
|
||||||
|
|
||||||
|
def update_db_model(cls, db_entry, wsme_entry):
|
||||||
|
if not db_entry or not wsme_entry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for attr in cls._wsme_attributes:
|
||||||
|
attr_name = attr.name
|
||||||
|
value = getattr(wsme_entry, attr_name)
|
||||||
|
|
||||||
|
if isinstance(value, wtypes.UnsetType):
|
||||||
|
continue
|
||||||
|
|
||||||
|
setattr(db_entry, attr_name, value)
|
||||||
|
|
||||||
|
return db_entry
|
||||||
|
|
||||||
|
|
||||||
|
class Project(_Base):
|
||||||
|
name = wtypes.text
|
||||||
|
description = wtypes.text
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectGroup(_Base):
|
||||||
|
name = wtypes.text
|
||||||
|
title = wtypes.text
|
||||||
|
projects = wtypes.ArrayType(Project)
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(_Base):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Task(_Base):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class StoryTag(_Base):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Comment(_Base):
|
||||||
|
#todo(nkonovalov): replace with a enum
|
||||||
|
action = wtypes.text
|
||||||
|
comment_type = wtypes.text
|
||||||
|
content = wtypes.text
|
||||||
|
|
||||||
|
story_id = int
|
||||||
|
author_id = int
|
||||||
|
|
||||||
|
|
||||||
|
class Story(_Base):
|
||||||
|
title = wtypes.text
|
||||||
|
description = wtypes.text
|
||||||
|
is_bug = bool
|
||||||
|
#todo(nkonovalov): replace with a enum
|
||||||
|
priority = wtypes.text
|
||||||
|
tasks = wtypes.ArrayType(Task)
|
||||||
|
comments = wtypes.ArrayType(Comment)
|
||||||
|
tags = wtypes.ArrayType(StoryTag)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_task(cls, story_id, task):
|
||||||
|
return cls.create_and_add_item("id", story_id, Task, task, "tasks")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_comment(cls, story_id, comment):
|
||||||
|
return cls.create_and_add_item("id", story_id, Comment, comment,
|
||||||
|
"comments")
|
||||||
|
|
||||||
|
|
||||||
|
class User(_Base):
|
||||||
|
username = wtypes.text
|
||||||
|
first_name = wtypes.text
|
||||||
|
last_name = wtypes.text
|
||||||
|
email = wtypes.text
|
||||||
|
is_staff = bool
|
||||||
|
is_active = bool
|
||||||
|
is_superuser = bool
|
||||||
|
last_login = datetime
|
||||||
|
#teams = wtypes.ArrayType(Team)
|
||||||
|
permissions = wtypes.ArrayType(Permission)
|
||||||
|
#tasks = wtypes.ArrayType(Task)
|
||||||
|
|
||||||
|
|
||||||
|
class Team(_Base):
|
||||||
|
name = wtypes.text
|
||||||
|
users = wtypes.ArrayType(User)
|
||||||
|
permissions = wtypes.ArrayType(Permission)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_user(cls, team_name, username):
|
||||||
|
return cls.add_item("name", team_name,
|
||||||
|
User, "username", username,
|
||||||
|
"users")
|
||||||
|
|
||||||
|
|
||||||
|
SQLALCHEMY_TO_WSME = {
|
||||||
|
sqlalchemy_models.Team: Team,
|
||||||
|
sqlalchemy_models.User: User,
|
||||||
|
sqlalchemy_models.ProjectGroup: ProjectGroup,
|
||||||
|
sqlalchemy_models.Project: Project,
|
||||||
|
sqlalchemy_models.Permission: Permission,
|
||||||
|
sqlalchemy_models.Story: Story,
|
||||||
|
sqlalchemy_models.Task: Task,
|
||||||
|
sqlalchemy_models.Comment: Comment,
|
||||||
|
sqlalchemy_models.StoryTag: StoryTag
|
||||||
|
}
|
||||||
|
|
||||||
|
# database mappings
|
||||||
|
WSME_TO_SQLALCHEMY = dict(
|
||||||
|
(v, k) for k, v in six.iteritems(SQLALCHEMY_TO_WSME)
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user