diff --git a/api/api.py b/api/api.py index 5474de54c3db00956af60308a5811a51863b4774..d9523bcadd5d8e11579c794966ccbb5999ca4020 100644 --- a/api/api.py +++ b/api/api.py @@ -25,9 +25,79 @@ log = config.log class Config(base.RequestHandler): def get(self): + """ + .. http:get:: /api/config + + Return public Scitran configuration information. + + :statuscode 200: no error + + + **Example request**: + + .. sourcecode:: http + + GET /api/config HTTP/1.1 + Host: demo.flywheel.io + Accept: */* + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept-Encoding + Content-Type: application/json; charset=utf-8 + {"site": {"central_url": "https://sdmc.scitran.io/api", "ssl_cert": null, "api_url": "https://10.240.0.2:443/api", "registered": false, "id": "local", "name": "BaliDemo"}, "modified": "2016-03-31T16:30:00.852000+00:00", "auth": {"id_endpoint": "https://www.googleapis.com/plus/v1/people/me/openIdConnect", "verify_endpoint": "https://www.googleapis.com/oauth2/v1/tokeninfo", "client_id": "949263322061-6q4fqi0m4ihkp1v5n6v8q9bef4gd0f1k.apps.googleusercontent.com", "auth_endpoint": "https://accounts.google.com/o/oauth2/auth"}, "created": "2016-03-31T16:30:00.852000+00:00"} + """ + return config.get_public_config() def get_js(self): + """ + .. http:get:: /api/config.js + + Return public Scitran configuration information in javascript format. + + :statuscode 200: no error + + + **Example request**: + + .. sourcecode:: http + + GET /api/config.js HTTP/1.1 + Host: demo.flywheel.io + Accept: */* + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept-Encoding + Content-Type: text/html; charset=utf-8 + config = { + "auth": { + "auth_endpoint": "https://accounts.google.com/o/oauth2/auth", + "client_id": "949263322061-6q4fqi0m4ihkp1v5n6v8q9bef4gd0f1k.apps.googleusercontent.com", + "id_endpoint": "https://www.googleapis.com/plus/v1/people/me/openIdConnect", + "verify_endpoint": "https://www.googleapis.com/oauth2/v1/tokeninfo" + }, + "created": "2016-03-31T16:30:00.852000+00:00", + "modified": "2016-03-31T16:30:00.852000+00:00", + "site": { + "api_url": "https://10.240.0.2:443/api", + "central_url": "https://sdmc.scitran.io/api", + "id": "local", + "name": "BaliDemo", + "registered": false, + "ssl_cert": null + } + """ + self.response.write( 'config = ' + json.dumps( self.get(), sort_keys=True, indent=4, separators=(',', ': '), default=util.custom_json_serializer,) + @@ -37,6 +107,33 @@ class Config(base.RequestHandler): class Version(base.RequestHandler): def get(self): + """ + .. http:get:: /api/version + + Return database schema in javascript format. + + :statuscode 200: no error + + + **Example request**: + + .. sourcecode:: http + + GET /api/version HTTP/1.1 + Host: demo.flywheel.io + Accept: */* + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept-Encoding + Content-Type: application/json; charset=utf-8 + {"_id": "version", "database": 2} + """ + return config.get_version() #regexes used in routing table: diff --git a/api/centralclient.py b/api/centralclient.py index 6003ade2533e4b20178344c46aa8c2a0ac7b160e..5c9fef657d602c408de85d5bca0aa5b7e4c47143 100644 --- a/api/centralclient.py +++ b/api/centralclient.py @@ -89,7 +89,34 @@ def clean_remotes(db, site_id): class CentralClient(base.RequestHandler): def sites(self): - """Return local and remote sites.""" + """ + .. http:get:: /api/sites + + Return local and remote sites. + + :statuscode 200: no error + + **Example request**: + + .. sourcecode:: http + + GET /api/sites HTTP/1.1 + Host: demo.flywheel.io + Accept: */* + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept-Encoding + Content-Type: application/json; charset=utf-8 + [{"onload": true, "_id": "local", "name": "BaliDemo"}] + + + """ + projection = ['name', 'onload'] # TODO onload for local is true site_id = config.get_item('site', 'id') @@ -107,6 +134,14 @@ class CentralClient(base.RequestHandler): return sites def register(self): + """ + .. http:post:: /api/register + + NOT IMPLEMENTED -- Return local and remote sites. + + :statuscode 404: Not Implemented + """ + self.abort(404, 'register endpoint is not implemented') # FIXME the code below should be properly ported using the new config # every request to this route is aborted at the moment diff --git a/api/download.py b/api/download.py index f6c44a6b24aad94cbbbcd57a0c26d5b4c4e9ef12..110d411e0737aeb5a373fbfb4542f791755af50c 100644 --- a/api/download.py +++ b/api/download.py @@ -153,6 +153,24 @@ class Download(base.RequestHandler): stream.close() def download(self): + """ + .. http:get:: /api/download + + Download GET Description... + + :statuscode 400: describe me + :statuscode 404: describe me + + + .. http:post:: /api/download + + Download POST Description... + + :statuscode 400: describe me + :statuscode 404: describe me + """ + + """ In downloads we use filters in the payload to exclude/include files. To pass a single filter, each of its conditions should be satisfied. diff --git a/api/handlers/listhandler.py b/api/handlers/listhandler.py index 15986396775bbdf338d88441787bbd7057d3ed3b..b90ed31122e1876127fdc260e19b39d3581566e1 100644 --- a/api/handlers/listhandler.py +++ b/api/handlers/listhandler.py @@ -352,6 +352,45 @@ class FileListHandler(ListHandler): return ticket def get(self, cont_name, list_name, **kwargs): + """ + .. http:get:: /api/(cont_name)/(cid)/files/(file_name) + + Gets the ticket used to download the file when the ticket is not provided. + + Downloads the file when the ticket is provided. + + :query ticket: should be empty + + :param cont_name: one of ``projects``, ``sessions``, ``acquisitions``, ``collections`` + :type cont_name: string + + :param cid: Container ID + :type cid: string + + :statuscode 200: no error + :statuscode 400: explain... + :statuscode 409: explain... + + **Example request**: + + .. sourcecode:: http + + GET /api/acquisitions/57081d06b386a6dc79ca383c/files/fMRI%20Loc%20Word%20Face%20Obj.zip?ticket= HTTP/1.1 + Host: demo.flywheel.io + Accept: */* + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept-Encoding + Content-Type: application/json; charset=utf-8 + {"ticket": "1e975e3d-21e9-41f4-bb97-261f03d35ba1"} + + """ + log.error('{} {} {}'.format(cont_name, list_name, kwargs)) _id = kwargs.pop('cid') container, permchecker, storage, _, _, keycheck = self._initialize_request(cont_name, list_name, _id) diff --git a/api/handlers/userhandler.py b/api/handlers/userhandler.py index 85adca4174d5926cffb356f903976c6145ef24b9..58a497c4424676ad747281bb0e00c8837b6ef09c 100644 --- a/api/handlers/userhandler.py +++ b/api/handlers/userhandler.py @@ -60,6 +60,39 @@ class UserHandler(base.RequestHandler): return result def put(self, _id): + """ + .. http:put:: /api/users/(uid) + + Update user + + :query root: explain... + + :param uid: User ID (email address) + :type uid: string + + :reqheader Authorization: required OAuth session token + + **Example request**: + + .. sourcecode:: http + + PUT /api/users/jdoe@gmail.com?root=true HTTP/1.1 + Host: demo.flywheel.io + Authorization: ya29..a356DasssSFG_FEbggasr435g54GG$33DFGSssghnj-HSsdfgs450nvsASAPinZCXVqertt + Content-Type: application/json;charset=UTF-8 + {"firstname":"John","lastname":"Doe","email":"jdoe@gmail.com","root":true} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json; charset=utf-8 + Content-Length: 15 + {"modified": 1} + + """ + self._init_storage() user = self._get_user(_id) permchecker = userauth.default(self, user) @@ -77,6 +110,35 @@ class UserHandler(base.RequestHandler): self.abort(404, 'User {} not updated'.format(_id)) def post(self): + """ + .. http:post:: /api/users + + Add user + :query root: explain... + + :reqheader Authorization: required OAuth session token + + **Example request**: + + .. sourcecode:: http + + POST /api/users?root=true HTTP/1.1 + Host: demo.flywheel.io + Authorization: ya29..a356DasssSFG_FEbggasr435g54GG$33DFGSssghnj-HSsdfgs450nvsASAPinZCXVqertt + Content-Type: application/json;charset=UTF-8 + {"_id":"jane.doe@gmail.com","firstname":"Jane","lastname":"Doe","email":"jane.doe@gmail.com"} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json; charset=utf-8 + Vary: Accept-Encoding + {"_id": "jane.doe@gmail.com"} + + """ + self._init_storage() permchecker = userauth.default(self) payload = self.request.json_body @@ -153,4 +215,3 @@ class UserHandler(base.RequestHandler): return user else: self.abort(404, 'user {} not found'.format(_id)) - diff --git a/api/root.py b/api/root.py index 4b83c0cfa50b4c9a41445f92b842d763fb2fd8b3..204acd5b93c2ee4e373fb40b67780e52a861358b 100644 --- a/api/root.py +++ b/api/root.py @@ -9,11 +9,208 @@ log = config.log class Root(base.RequestHandler): def head(self): - """Return 200 OK.""" + """ + .. http:head:: /api + + Confirm endpoint is ready for requests + + :statuscode 200: no error + """ + pass def get(self): - """Return API documentation""" + """ + .. http:get:: /api + + Return API documentation + + :statuscode 200: no error + + **Example request**: + + .. sourcecode:: http + + GET /api HTTP/1.1 + Host: demo.flywheel.io + Accept: text/html + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept-Encoding + Content-Type: text/html + <html> + <head> + <title>SciTran API</title> + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> + <style type="text/css"> + table {width:0%; border-width:1px; padding: 0;border-collapse: collapse;} + table tr {border-top: 1px solid #b8b8b8; background-color: white; margin: 0; padding: 0;} + table tr:nth-child(2n) {background-color: #f8f8f8;} + table thead tr :last-child {width:100%;} + table tr th {font-weight: bold; border: 1px solid #b8b8b8; background-color: #cdcdcd; margin: 0; padding: 6px 13px;} + table tr th {font-weight: bold; border: 1px solid #b8b8b8; background-color: #cdcdcd; margin: 0; padding: 6px 13px;} + table tr td {border: 1px solid #b8b8b8; margin: 0; padding: 6px 13px;} + table tr th :first-child, table tr td :first-child {margin-top: 0;} + table tr th :last-child, table tr td :last-child {margin-bottom: 0;} + </style> + </head> + <body style="min-width:900px"> + <form name="username" action="" method="get"> + Username: <input type="text" name="user"> + Root: <input type="checkbox" name="root" value="1"> + <input type="submit" value="Generate Custom Links"> + </form> + <table> + <thead> + <tr> + <th align="left">Resource</th> + <th align="left">Description</th> + </tr> + </thead> + <tbody> + <tr> + <td align="left"><a href="/api/sites">/sites</a></td> + <td align="left">local and remote sites</td> + </tr> + <tr> + <td align="left">/download</td> + <td align="left">download</td> + </tr> + <tr> + <td align="left"><a href="/api/users">/users</a></td> + <td align="left">list of users</td> + </tr> + <tr> + <td align="left"><a href="/api/users/self">/users/self</a></td> + <td align="left">user identity</td> + </tr> + <tr> + <td align="left"><a href="/api/users/roles">/users/roles</a></td> + <td align="left">user roles</td> + </tr> + <tr> + <td align="left"><a href="/api/users/*&lt;uid&gt;*">/users/<em><uid></em></a></td> + <td align="left">details for user <em><uid></em></td> + </tr> + <tr> + <td align="left"><a href="/api/users/*&lt;uid&gt;*/groups">/users/<em><uid></em>/groups</a></td> + <td align="left">groups for user <em><uid></em></td> + </tr> + <tr> + <td align="left"><a href="/api/users/*&lt;uid&gt;*/projects">/users/<em><uid></em>/projects</a></td> + <td align="left">projects for user <em><uid></em></td> + </tr> + <tr> + <td align="left"><a href="/api/groups">/groups</a></td> + <td align="left">list of groups</td> + </tr> + <tr> + <td align="left">/groups/<em><gid></em></td> + <td align="left">details for group <em><gid></em></td> + </tr> + <tr> + <td align="left">/groups/<em><gid></em>/projects</td> + <td align="left">list of projects for group <em><gid></em></td> + </tr> + <tr> + <td align="left">/groups/<em><gid></em>/sessions</td> + <td align="left">list of sessions for group <em><gid></em></td> + </tr> + <tr> + <td align="left"><a href="/api/projects">/projects</a></td> + <td align="left">list of projects</td> + </tr> + <tr> + <td align="left"><a href="/api/projects/groups">/projects/groups</a></td> + <td align="left">groups for projects</td> + </tr> + <tr> + <td align="left"><a href="/api/projects/schema">/projects/schema</a></td> + <td align="left">schema for single project</td> + </tr> + <tr> + <td align="left">/projects/<em><pid></em></td> + <td align="left">details for project <em><pid></em></td> + </tr> + <tr> + <td align="left">/projects/<em><pid></em>/sessions</td> + <td align="left">list sessions for project <em><pid></em></td> + </tr> + <tr> + <td align="left"><a href="/api/sessions">/sessions</a></td> + <td align="left">list of sessions</td> + </tr> + <tr> + <td align="left"><a href="/api/sessions/schema">/sessions/schema</a></td> + <td align="left">schema for single session</td> + </tr> + <tr> + <td align="left">/sessions/<em><sid></em></td> + <td align="left">details for session <em><sid></em></td> + </tr> + <tr> + <td align="left">/sessions/<em><sid></em>/move</td> + <td align="left">move session <em><sid></em> to a different project</td> + </tr> + <tr> + <td align="left">/sessions/<em><sid></em>/acquisitions</td> + <td align="left">list acquisitions for session <em><sid></em></td> + </tr> + <tr> + <td align="left"><a href="/api/acquisitions/schema">/acquisitions/schema</a></td> + <td align="left">schema for single acquisition</td> + </tr> + <tr> + <td align="left">/acquisitions/<em><aid></em></td> + <td align="left">details for acquisition <em><aid></em></td> + </tr> + <tr> + <td align="left"><a href="/api/collections">/collections</a></td> + <td align="left">list of collections</td> + </tr> + <tr> + <td align="left"><a href="/api/collections/schema">/collections/schema</a></td> + <td align="left">schema for single collection</td> + </tr> + <tr> + <td align="left">/collections/<em><cid></em></td> + <td align="left">details for collection <em><cid></em></td> + </tr> + <tr> + <td align="left">/collections/<em><cid></em>/sessions</td> + <td align="left">list of sessions for collection <em><cid></em></td> + </tr> + <tr> + <td align="left">/collections/<em><cid></em>/acquisitions</td> + <td align="left">list of acquisitions for collection <em><cid></em></td> + </tr> + <tr> + <td align="left"><a href="/api/schema/group">/schema/group</a></td> + <td align="left">group schema</td> + </tr> + <tr> + <td align="left"><a href="/api/schema/user">/schema/user</a></td> + <td align="left">user schema</td> + </tr> + </tbody> + </table></body> + </html> + + + :query sort: one of ``hit``, ``created-at`` + :query offset: offset number. default is 0 + :query limit: limit number. default is 30 + :reqheader Accept: the response content type depends on + :mailheader:`Accept` header + :reqheader Authorization: optional OAuth token to authenticate + :resheader Content-Type: this depends on :mailheader:`Accept` + header of request + """ + resources = """ Resource | Description :-----------------------------------|:----------------------- diff --git a/api/upload.py b/api/upload.py index 38f97aadb213ba8af53032b67e1d82e5fba78c07..6c5949cc79a999bae47e9d6ed28d9fbd7a631ef7 100644 --- a/api/upload.py +++ b/api/upload.py @@ -176,7 +176,15 @@ class Upload(base.RequestHandler): log.info('Received %s [%s, %s/s] from %s' % (file_store.filename, util.hrsize(file_store.size), util.hrsize(throughput), self.request.client_addr)) def upload(self, strategy): - """Receive a sortable reaper upload.""" + """ + .. http:post:: /api/upload/<strategy:label|uid> + + Receive a sortable reaper upload. + + :statuscode 402: no error + :statuscode 500: no error + """ + if not self.superuser_request: self.abort(402, 'uploads must be from an authorized drone') @@ -191,7 +199,17 @@ class Upload(base.RequestHandler): def engine(self): """ - URL format: api/engine?level=<container_type>&id=<container_id>&job=<job_id> + .. http:post:: /api/engine + + Confirm endpoint is ready for requests + + :query level: container_type + :query id: container_id + :query job: job_id + + :statuscode 400: describe me + :statuscode 402: describe me + :statuscode 404: describe me """ if not self.superuser_request: @@ -268,8 +286,14 @@ class Upload(base.RequestHandler): def clean_packfile_tokens(self): """ - Clean up expired upload tokens and invalid token directories. + .. http:post:: /api/clean-packfiles + + Clean up expired upload tokens and invalid token directories. + + :statuscode 402: describe me + """ + """ Ref placer.TokenPlacer and FileListHandler.packfile_start for context. """