diff --git a/docs/uri-scheme-api-content.rst b/docs/uri-scheme-api-content.rst index 859a661c..7c530988 100644 --- a/docs/uri-scheme-api-content.rst +++ b/docs/uri-scheme-api-content.rst @@ -1,338 +1,14 @@ Content ------- -.. http:get:: /api/1/content/known/(sha1)[,(sha1), ...,(sha1)]/ +.. autosimple:: swh.web.api.views.content.api_check_content_known - Check whether some content(s) (aka "blob(s)") is present in the SWH archive - based on its *sha1* checksum. +.. autosimple:: swh.web.api.views.content.api_content_metadata - :param string sha1: hexadecimal representation of the *sha1* checksum value - of the content to check existence. Multiple values can be provided separated - by ','. +.. autosimple:: swh.web.api.views.content.api_content_raw - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request +.. autosimple:: swh.web.api.views.content.api_content_filetype - :>json array search_res: array holding the search result for each provided *sha1* - :>json object search_stats: some statistics regarding the number of *sha1* provided - and the percentage of those found in the SWH archive +.. autosimple:: swh.web.api.views.content.api_content_language - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *sha1* has been provided - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`content/known/dc2830a9e72f23c1dfebef4413003221baa5fb62,0c3f19cb47ebfbe643fb19fa94c874d18fa62d12/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "search_res": [ - { - "found": true, - "sha1": "dc2830a9e72f23c1dfebef4413003221baa5fb62" - }, - { - "found": true, - "sha1": "0c3f19cb47ebfbe643fb19fa94c874d18fa62d12" - } - ], - "search_stats": { - "nbfiles": 2, - "pct": 100.0 - } - } - - -.. http:get:: /api/1/content/[(hash_type):](hash)/ - - Get information about a content (aka a "blob") object. - In the SWH archive, a content object is identified based on checksum - values computed using various hashing algorithms. - - :param string hash_type: optional parameter specifying which hashing algorithm has been used - to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* - or *blake2s256*. If that parameter is not provided, it is assumed that the - hashing algorithm used is *sha1*. - :param string hash: hexadecimal representation of the checksum value computed with - the specified hashing algorithm. - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - :>json object checksums: object holding the computed checksum values for the requested content - :>json string data_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/raw/` - for downloading the content raw bytes - :>json string filetype_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/filetype/` - for getting information about the content MIME type - :>json string language_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/language/` - for getting information about the programming language used in the content - :>json number length: length of the content in bytes - :>json string license_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/license/` - for getting information about the license of the content - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *hash_type* or *hash* has been provided - :statuscode 404: requested content can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`content/sha1_git:fe95a46679d128ff167b7c55df5d02356c5a1ae1/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "checksums": { - "blake2s256": "791e07fcea240ade6dccd0a9309141673c31242cae9c237cf3855e151abc78e9", - "sha1": "dc2830a9e72f23c1dfebef4413003221baa5fb62", - "sha1_git": "fe95a46679d128ff167b7c55df5d02356c5a1ae1", - "sha256": "b5c7fe0536f44ef60c8780b6065d30bca74a5cd06d78a4a71ba1ad064770f0c9" - }, - "data_url": "/api/1/content/sha1_git:fe95a46679d128ff167b7c55df5d02356c5a1ae1/raw/", - "filetype_url": "/api/1/content/sha1_git:fe95a46679d128ff167b7c55df5d02356c5a1ae1/filetype/", - "language_url": "/api/1/content/sha1_git:fe95a46679d128ff167b7c55df5d02356c5a1ae1/language/", - "length": 151810, - "license_url": "/api/1/content/sha1_git:fe95a46679d128ff167b7c55df5d02356c5a1ae1/license/", - "status": "visible" - } - -.. http:get:: /api/1/content/[(hash_type):](hash)/raw/ - - Get the raw content of a content object (aka a "blob"), as a byte sequence. - - :param string hash_type: optional parameter specifying which hashing algorithm has been used - to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* - or *blake2s256*. If that parameter is not provided, it is assumed that the - hashing algorithm used is *sha1*. - :param string hash: hexadecimal representation of the checksum value computed with - the specified hashing algorithm. - :query string filename: if provided, the downloaded content will get that filename - - :resheader Content-Type: application/octet-stream - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *hash_type* or *hash* has been provided - :statuscode 404: requested content can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/raw/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-disposition: attachment; filename=content_sha1_dc2830a9e72f23c1dfebef4413003221baa5fb62_raw - Content-Type: application/octet-stream - - /* 'dir', 'vdir' and 'ls' directory listing programs for GNU. - Copyright (C) 1985-2015 Free Software Foundation, Inc. - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . */ - - ... - -.. http:get:: /api/1/content/[(hash_type):](hash)/filetype/ - - Get information about the detected MIME type of a content object. - - :param string hash_type: optional parameter specifying which hashing algorithm has been used - to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* - or *blake2s256*. If that parameter is not provided, it is assumed that the - hashing algorithm used is *sha1*. - :param string hash: hexadecimal representation of the checksum value computed with - the specified hashing algorithm. - - :>json object content_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/` for - getting information about the content - :>json string encoding: the detected content encoding - :>json string id: the *sha1* identifier of the content - :>json string mimetype: the detected MIME type of the content - :>json object tool: information about the tool used to detect the content filetype - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *hash_type* or *hash* has been provided - :statuscode 404: requested content can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/filetype/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "content_url": "/api/1/content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/", - "encoding": "us-ascii", - "id": "dc2830a9e72f23c1dfebef4413003221baa5fb62", - "mimetype": "text/x-c", - "tool": { - "configuration": { - "command_line": "file --mime " - }, - "id": 7, - "name": "file", - "version": "5.22" - } - } - -.. http:get:: /api/1/content/[(hash_type):](hash)/language/ - - Get information about the programming language used in a content object. - - :param string hash_type: optional parameter specifying which hashing algorithm has been used - to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* - or *blake2s256*. If that parameter is not provided, it is assumed that the - hashing algorithm used is *sha1*. - :param string hash: hexadecimal representation of the checksum value computed with - the specified hashing algorithm. - - :>json object content_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/` for - getting information about the content - :>json string id: the *sha1* identifier of the content - :>json string lang: the detected programming language if any - :>json object tool: information about the tool used to detect the programming language - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *hash_type* or *hash* has been provided - :statuscode 404: requested content can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/language/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "content_url": "/api/1/content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/", - "id": "dc2830a9e72f23c1dfebef4413003221baa5fb62", - "lang": "c", - "tool": { - "configuration": { - "debian-package": "python3-pygments", - "max_content_size": 10240, - "type": "library" - }, - "id": 8, - "name": "pygments", - "version": "2.0.1+dfsg-1.1+deb8u1" - } - } - - -.. http:get:: /api/1/content/[(hash_type):](hash)/license/ - - Get information about the license of a content object. - - :param string hash_type: optional parameter specifying which hashing algorithm has been used - to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* - or *blake2s256*. If that parameter is not provided, it is assumed that the - hashing algorithm used is *sha1*. - :param string hash: hexadecimal representation of the checksum value computed with - the specified hashing algorithm. - - :>json object content_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/` for - getting information about the content - :>json string id: the *sha1* identifier of the content - :>json array licenses: array of strings containing the detected license names if any - :>json object tool: information about the tool used to detect the license - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *hash_type* or *hash* has been provided - :statuscode 404: requested content can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/license/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "content_url": "/api/1/content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/", - "id": "dc2830a9e72f23c1dfebef4413003221baa5fb62", - "licenses": [ - "GPL-3.0+" - ], - "tool": { - "configuration": { - "command_line": "nomossa " - }, - "id": 1, - "name": "nomos", - "version": "3.1.0rc2-31-ga2cbb8c" - } - } +.. autosimple:: swh.web.api.views.content.api_content_license diff --git a/docs/uri-scheme-api-directory.rst b/docs/uri-scheme-api-directory.rst index 78f46c18..85ac19d4 100644 --- a/docs/uri-scheme-api-directory.rst +++ b/docs/uri-scheme-api-directory.rst @@ -1,87 +1,4 @@ Directory --------- -.. http:get:: /api/1/directory/(sha1_git)/[(path)/] - - Get information about directory objects. - Directories are identified by *sha1* checksums, compatible with Git directory identifiers. - See :func:`swh.model.identifiers.directory_identifier` in our data model module for details - about how they are computed. - - When given only a directory identifier, this endpoint returns information about the directory itself, - returning its content (usually a list of directory entries). When given a directory identifier and a - path, this endpoint returns information about the directory entry pointed by the relative path, - starting path resolution from the given directory. - - :param string sha1_git: hexadecimal representation of the directory *sha1_git* identifier - :param string path: optional parameter to get information about the directory entry - pointed by that relative path - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - :>jsonarr object checksums: object holding the computed checksum values for a directory entry - (only for file entries) - :>jsonarr string dir_id: *sha1_git* identifier of the requested directory - :>jsonarr number length: length of a directory entry in bytes (only for file entries) - for getting information about the content MIME type - :>jsonarr string name: the directory entry name - :>jsonarr number perms: permissions for the directory entry - :>jsonarr string target: *sha1_git* identifier of the directory entry - :>jsonarr string target_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/` - or :http:get:`/api/1/directory/(sha1_git)/[(path)/]` depending on the directory entry type - :>jsonarr string type: the type of the directory entry, can be either *dir*, *file* or *rev* - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *hash_type* or *hash* has been provided - :statuscode 404: requested directory can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`directory/977fc4b98c0e85816348cebd3b12026407c368b6/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - [ - { - "checksums": { - "sha1": "e2d79ae437210941840f49966497cc348c7e817f", - "sha1_git": "58471109208922c9ee8c4b06135725f03ed16814", - "sha256": "2b7001f4819e898776b45b2fa3411018b7bc24e38afbb351691c32508eb2ae5d" - }, - "dir_id": "977fc4b98c0e85816348cebd3b12026407c368b6", - "length": 582, - "name": ".bzrignore", - "perms": 33188, - "status": "visible", - "target": "58471109208922c9ee8c4b06135725f03ed16814", - "target_url": "/api/1/content/sha1_git:58471109208922c9ee8c4b06135725f03ed16814/", - "type": "file" - }, - { - "checksums": { - "sha1": "f47aabb47381119cf72add7633bc095ca2cd030d", - "sha1_git": "2106da61725973b81a63a817ec6f245706af4353", - "sha256": "4f0475fac23bcd3ebceceecffb0d4facc5a413f6d9a0287185fb75638b8e9c69" - }, - "dir_id": "977fc4b98c0e85816348cebd3b12026407c368b6", - "length": 453, - "name": ".codecov.yml", - "perms": 33188, - "status": "visible", - "target": "2106da61725973b81a63a817ec6f245706af4353", - "target_url": "/api/1/content/sha1_git:2106da61725973b81a63a817ec6f245706af4353/", - "type": "file" - }, - - ] +.. autosimple:: swh.web.api.views.directory.api_directory diff --git a/docs/uri-scheme-api-origin.rst b/docs/uri-scheme-api-origin.rst index bbc3f867..0fa7d864 100644 --- a/docs/uri-scheme-api-origin.rst +++ b/docs/uri-scheme-api-origin.rst @@ -1,278 +1,10 @@ Origin ------ -.. http:get:: /api/1/origin/(origin_id)/ +.. autosimple:: swh.web.api.views.origin.api_origin - Get information about a software origin from its unique (but otherwise meaningless) - identifier. +.. autosimple:: swh.web.api.views.origin.api_origin_search - :param int origin_id: a SWH origin identifier +.. autosimple:: swh.web.api.views.origin.api_origin_visits - :>json number id: the origin unique identifier - :>json string origin_visits_url: link to in order to get information about the SWH - visits for that origin - :>json string type: the type of software origin (*git*, *svn*, *hg*, *deb*, *ftp*, ...) - :>json string url: the origin canonical url - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - :statuscode 200: no error - :statuscode 404: requested origin can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`origin/1/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "id": 1, - "origin_visits_url": "/api/1/origin/1/visits/", - "type": "git", - "url": "https://github.com/hylang/hy" - } - -.. http:get:: /api/1/origin/(origin_type)/url/(origin_url)/ - - Get information about a software origin from its type and canonical url. - - :param string origin_type: the origin type (*git*, *svn*, *hg*, *deb*, *ftp*, ...) - :param string origin_url: the origin url - - :>json number id: the origin unique identifier - :>json string origin_visits_url: link to in order to get information about the SWH - visits for that origin - :>json string type: the type of software origin (*git*, *svn*, *hg*, *deb*, *ftp*, ...) - :>json string url: the origin canonical url - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 404: requested origin can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`origin/git/url/https://github.com/python/cpython/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "id": 13706355, - "origin_visits_url": "/api/1/origin/13706355/visits/", - "type": "git", - "url": "https://github.com/python/cpython" - } - -.. http:get:: /api/1/origin/search/(url_pattern)/ - - Search for software origins whose urls contain a provided string - pattern or match a provided regular expression. - The search is performed in a case insensitive way. - - :param string url_pattern: a string pattern or a regular expression - :query int offset: the number of found origins to skip before returning results - :query int limit: the maximum number of found origins to return - :query boolean regexp: if true, consider provided pattern as a regular expression - and search origins whose urls match it - - :>jsonarr number id: the origin unique identifier - :>jsonarr string origin_visits_url: link to in order to get information about the SWH - visits for that origin - :>jsonarr string type: the type of software origin (*git*, *svn*, *hg*, *deb*, *ftp*, ...) - :>jsonarr string url: the origin canonical url - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`origin/search/python/?limit=2` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - [ - { - "type": "git", - "origin_visits_url": "/api/1/origin/220/visits/", - "id": 220, - "url": "https://github.com/neon670/python.dev" - }, - { - "type": "git", - "origin_visits_url": "/api/1/origin/328/visits/", - "id": 328, - "url": "https://github.com/aur-archive/python-werkzeug" - } - ] - -.. http:get:: /api/1/origin/(origin_id)/visits/ - - Get information about all visits of a software origin. - Visits are returned sorted in descending order according - to their date. - - :param int origin_id: a SWH origin identifier - :query int per_page: specify the number of visits to list, for pagination purposes - :query int last_visit: visit to start listing from, for pagination purposes - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - :resheader Link: indicates that a subsequent result page is available and contains - the url pointing to it - - :>jsonarr string date: ISO representation of the visit date (in UTC) - :>jsonarr number id: the unique identifier of the origin - :>jsonarr string origin_visit_url: link to :http:get:`/api/1/origin/(origin_id)/visit/(visit_id)/` - in order to get information about the visit - :>jsonarr string snapshot: the snapshot identifier of the visit - :>jsonarr string snapshot_url: link to :http:get:`/api/1/snapshot/(snapshot_id)/` - in order to get information about the snapshot of the visit - :>jsonarr string status: status of the visit (either *full*, *partial* or *ongoing*) - :>jsonarr number visit: the unique identifier of the visit - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 404: requested origin can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`origin/1/visits/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Link: ; rel="next" - Content-Type: application/json - - [ - { - "date": "2015-08-04T22:26:14.804009+00:00", - "origin": 1, - "origin_visit_url": "/api/1/origin/1/visit/1/", - "snapshot": "584b2fe3ce6218a96892e73bd76c2966bbc2a797", - "snapshot_url": "/api/1/snapshot/584b2fe3ce6218a96892e73bd76c2966bbc2a797/", - "status": "full", - "visit": 1 - }, - { - "date": "2016-02-22T16:56:16.725068+00:00", - "metadata": {}, - "origin": 1, - "origin_visit_url": "/api/1/origin/1/visit/2/", - "snapshot": "9d502b5a193c33b59b22be3c75f73472867b0f8e", - "snapshot_url": "/api/1/snapshot/9d502b5a193c33b59b22be3c75f73472867b0f8e/", - "status": "full", - "visit": 2 - }, - ] - -.. http:get:: /api/1/origin/(origin_id)/visit/(visit_id)/ - - Get information about a specific visit of a software origin. - - :param int origin_id: a SWH origin identifier - :param int visit_id: a visit identifier - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - :>json string date: ISO representation of the visit date (in UTC) - :>json object occurrences: object containing all branches associated to the origin found - during the visit, for each of them the associated SWH target type and id are given - but also a link to get information about that target - :>json number origin: the origin unique identifier - :>json string origin_url: link to get information about the origin - :>jsonarr string snapshot: the snapshot identifier of the visit - :>jsonarr string snapshot_url: link to :http:get:`/api/1/snapshot/(snapshot_id)/` - in order to get information about the snapshot of the visit - :>json string status: status of the visit (either *full*, *partial* or *ongoing*) - :>json number visit: the unique identifier of the visit - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 404: requested origin or visit can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`origin/1500/visit/1/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "date": "2015-08-23T17:48:46.800813+00:00", - "occurrences": { - "refs/heads/master": { - "target": "83c20a6a63a7ebc1a549d367bc07a61b926cecf3", - "target_type": "revision", - "target_url": "/api/1/revision/83c20a6a63a7ebc1a549d367bc07a61b926cecf3/" - }, - "refs/heads/wiki": { - "target": "71f667aeb5d02562f2fa0941ad91df69c474ff3b", - "target_type": "revision", - "target_url": "/api/1/revision/71f667aeb5d02562f2fa0941ad91df69c474ff3b/" - }, - "refs/tags/dpkt-1.6": { - "target": "7fc0fd582812af36064d1c85fe51e33227920479", - "target_type": "revision", - "target_url": "/api/1/revision/7fc0fd582812af36064d1c85fe51e33227920479/" - }, - "refs/tags/dpkt-1.7": { - "target": "0c9dbfbc0974ec8ac1d8253aa1092366a03633a8", - "target_type": "revision", - "target_url": "/api/1/revision/0c9dbfbc0974ec8ac1d8253aa1092366a03633a8/" - } - }, - "origin": 1500, - "origin_url": "/api/1/origin/1500/", - "snapshot": "6a3a2cf0b2b90ce7ae1cf0a221ed68035b686f5a", - "snapshot_url": "/api/1/snapshot/6a3a2cf0b2b90ce7ae1cf0a221ed68035b686f5a/", - "status": "full", - "visit": 1 - } +.. autosimple:: swh.web.api.views.origin.api_origin_visit diff --git a/docs/uri-scheme-api-person.rst b/docs/uri-scheme-api-person.rst index 535d2f11..cec4e9de 100644 --- a/docs/uri-scheme-api-person.rst +++ b/docs/uri-scheme-api-person.rst @@ -1,42 +1,4 @@ Person ------ -.. http:get:: /api/1/person/(person_id)/ - - Get information about a person in the SWH archive. - - :param int person_id: a SWH person identifier - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - :>json string email: the email of the person - :>json string fullname: the full name of the person: combination of its name and email - :>json number id: the unique identifier of the person - :>json string name: the name of the person - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 404: requested person can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`person/8275/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "email": "torvalds@linux-foundation.org", - "fullname": "Linus Torvalds ", - "id": 8275, - "name": "Linus Torvalds" - } +.. autosimple:: swh.web.api.views.person.api_person diff --git a/docs/uri-scheme-api-release.rst b/docs/uri-scheme-api-release.rst index 041da523..8a839836 100644 --- a/docs/uri-scheme-api-release.rst +++ b/docs/uri-scheme-api-release.rst @@ -1,64 +1,5 @@ Release ------- -.. http:get:: /api/1/release/(sha1_git)/ +.. autosimple:: swh.web.api.views.release.api_release - Get information about a release in the SWH archive. - Releases are identified by *sha1* checksums, compatible with Git tag identifiers. - See :func:`swh.model.identifiers.release_identifier` in our data model module for details - about how they are computed. - - :param string sha1_git: hexadecimal representation of the release *sha1_git* identifier - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - :>json object author: information about the author of the release - :>json string author_url: link to :http:get:`/api/1/person/(person_id)/` to get - information about the author of the release - :>json string date: ISO representation of the release date (in UTC) - :>json string id: the release unique identifier - :>json string message: the message associated to the release - :>json string name: the name of the release - :>json string target: the target identifier of the release - :>json string target_type: the type of the target, can be either *release*, - *revision*, *content*, *directory* - :>json string target_url: a link to the adequate api url based on the target type - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *sha1_git* value has been provided - :statuscode 404: requested release can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`release/208f61cc7a5dbc9879ae6e5c2f95891e270f09ef/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "author": { - "email": "nad@python.org", - "fullname": "Ned Deily ", - "id": 8318464, - "name": "Ned Deily" - }, - "author_url": "/api/1/person/8318464/", - "date": "2017-03-21T02:46:28-04:00", - "id": "208f61cc7a5dbc9879ae6e5c2f95891e270f09ef", - "message": "Tag v3.6.1\n-----BEGIN PGP SIGNATURE-----\n\niQIcBAABCgAGBQJY0MxgAAoJEC00fqaqZUIdkZ0QAJw9PR++cbpS3Pt8QrmgS+xG\nPxrZ1yPPNPNSfbmRLWOlHJ0nBzFPVXUWdrqnevmZVRghyrc78sjuBL8QczYsum22\n1B6X/63vX3dI9yj8FR5nldEYPBMOOD6ryObWoKMeqyQT3LhAqxIU/9oqAsbx+ZYw\nrXmRTuypenmZabq3yIv2hORMFgcS7JZFuVb181b0Cihji/7l+WRI9hkGO8POBeFq\ntfJ16beH8hbbDw/+MLpwJifsALWsQOqnWt2/C8tJeHtMX+FLuJflwcIwotv73E22\nulmpXNwTNxnK5l5/C9JC6kr5nN9VJatVpSpe6dftAmTy16O5OrADtePZYxOZ7S3X\n6ipOaiKl3s/2oykkmasxPeaVXllbWgd2UGqIBlAUxM6rVD/4DyVDUHqbDotQD8Kz\nZ8nSFxou1ZdRTSlC26ToGCNc+B6bqv9GTC1hph/ijJkhvXfIC9X1fc/uO1wrV+wB\ni2dxXKh1mQCXuogNAx6rv7gPaXbPgDHob7Tlvo5Ddhr7rQoAaMjceGfUMOTORSqO\nR4ssE6yyNASQtMjW+Y5WeVEgtX7ttGKBsgD0PsrZTCjnZfJkFtZGUyfkdwNzLK8v\nRBqi1r+tEuR5tpin4h+erdlVjeMhVMQZOhBYmxY2Ge70PMVrOz4KaFY1GD+aaxt7\n+PfOKUxMYGKvogv7gD/3\n=Peec\n-----END PGP SIGNATURE-----\n", - "name": "v3.6.1", - "synthetic": false, - "target": "69c0db5050f623e8895b72dfe970392b1f9a0e2e", - "target_type": "revision", - "target_url": "/api/1/revision/69c0db5050f623e8895b72dfe970392b1f9a0e2e/" - } diff --git a/docs/uri-scheme-api-revision.rst b/docs/uri-scheme-api-revision.rst index b79a0ad5..3e176ee4 100644 --- a/docs/uri-scheme-api-revision.rst +++ b/docs/uri-scheme-api-revision.rst @@ -1,477 +1,12 @@ Revision -------- -.. http:get:: /api/1/revision/(sha1_git)/ +.. autosimple:: swh.web.api.views.revision.api_revision - Get information about a revision in the SWH archive. - Releases are identified by *sha1* checksums, compatible with Git commit identifiers. - See :func:`swh.model.identifiers.revision_identifier` in our data model module for details - about how they are computed. +.. autosimple:: swh.web.api.views.revision.api_revision_directory - :param string sha1_git: hexadecimal representation of the revision *sha1_git* identifier - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - :>json object author: information about the author of the revision - :>json string author_url: link to :http:get:`/api/1/person/(person_id)/` to get - information about the author of the revision - :>json object committer: information about the committer of the revision - :>json string committer_url: link to :http:get:`/api/1/person/(person_id)/` to get - information about the committer of the revision - :>json string committer_date: ISO representation of the commit date (in UTC) - :>json string date: ISO representation of the revision date (in UTC) - :>json string directory: the unique identifier that revision points to - :>json string directory_url: link to :http:get:`/api/1/directory/(sha1_git)/[(path)/]` - to get information about the directory associated to the revision - :>json string id: the revision unique identifier - :>json boolean merge: whether or not the revision corresponds to a merge commit - :>json string message: the message associated to the revision - :>json array parents: the parents of the revision, i.e. the previous revisions - that head directly to it, each entry of that array contains an unique parent - revision identifier but also a link to :http:get:`/api/1/revision/(sha1_git)/` - to get more informations about it - :>json string type: the type of the revision - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` +.. autosimple:: swh.web.api.views.revision.api_revision_log - :statuscode 200: no error - :statuscode 400: an invalid *sha1_git* value has been provided - :statuscode 404: requested revision can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "author": { - "email": "nicolas.dandrimont@crans.org", - "fullname": "Nicolas Dandrimont ", - "id": 42, - "name": "Nicolas Dandrimont" - }, - "author_url": "/api/1/person/42/", - "committer": { - "email": "nicolas.dandrimont@crans.org", - "fullname": "Nicolas Dandrimont ", - "id": 42, - "name": "Nicolas Dandrimont" - }, - "committer_date": "2014-08-18T18:18:25+02:00", - "committer_url": "/api/1/person/42/", - "date": "2014-08-18T18:18:25+02:00", - "directory": "9f2e5898e00a66e6ac11033959d7e05b1593353b", - "directory_url": "/api/1/directory/9f2e5898e00a66e6ac11033959d7e05b1593353b/", - "history_url": "/api/1/revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/log/", - "id": "aafb16d69fd30ff58afdd69036a26047f3aebdc6", - "merge": true, - "message": "Merge branch 'master' into pr/584\n", - "metadata": {}, - "parents": [ - { - "id": "26307d261279861c2d9c9eca3bb38519f951bea4", - "url": "/api/1/revision/26307d261279861c2d9c9eca3bb38519f951bea4/" - }, - { - "id": "37fc9e08d0c4b71807a4f1ecb06112e78d91c283", - "url": "/api/1/revision/37fc9e08d0c4b71807a4f1ecb06112e78d91c283/" - } - ], - "synthetic": false, - "type": "git", - "url": "/api/1/revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/" - } - -.. http:get:: /api/1/revision/(sha1_git)/directory/[(path)/] - - Get information about directory (entry) objects associated to revisions. - Each revision is associated to a single "root" directory. - This endpoint behaves like :http:get:`/api/1/directory/(sha1_git)/[(path)/]`, - but operates on the root directory associated to a given revision. - - :param string sha1_git: hexadecimal representation of the revision *sha1_git* identifier - :param string path: optional parameter to get information about the directory entry - pointed by that relative path - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - :>json array content: directory entries as returned by :http:get:`/api/1/directory/(sha1_git)/[(path)/]` - :>json string path: path of directory from the revision root one - :>json string revision: the unique revision identifier - :>json string type: the type of the directory - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *sha1_git* value has been provided - :statuscode 404: requested revision can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`revision/f1b94134a4b879bc55c3dacdb496690c8ebdc03f/directory/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "content": [ - { - "checksums": { - "sha1": "8de896f1d96b05c0cc3ea5233fdc232e90978c52", - "sha1_git": "f3a32d36d443cbb63ffb1bff743a890c181f482a", - "sha256": "a2ea552b2ed62b7f77cf47dae805d491fdb684ed3bbe297fcc68cbee755a5d10" - }, - "dir_id": "778d9438465328f7c2ffe1c9d791f5b83a194c39", - "file_url": "/api/1/revision/f1b94134a4b879bc55c3dacdb496690c8ebdc03f/directory/.gitattributes/", - "length": 73, - "name": ".gitattributes", - "perms": 33188, - "status": "visible", - "target": "f3a32d36d443cbb63ffb1bff743a890c181f482a", - "target_url": "/api/1/content/sha1_git:f3a32d36d443cbb63ffb1bff743a890c181f482a/", - "type": "file" - }, - { - "checksums": { - "sha1": "d493cd8f6de7611b9f0ef2b5cdf80d85adcc7917", - "sha1_git": "0b0ee9cc20323d3e4206eb3172f111bb211274e3", - "sha256": "4d6aaab1da470f61b92134d8b78a80376ae1ab74ec6a23a045e039065eafafd9" - }, - "dir_id": "778d9438465328f7c2ffe1c9d791f5b83a194c39", - "file_url": "/api/1/revision/f1b94134a4b879bc55c3dacdb496690c8ebdc03f/directory/.gitignore/", - "length": 452, - "name": ".gitignore", - "perms": 33188, - "status": "visible", - "target": "0b0ee9cc20323d3e4206eb3172f111bb211274e3", - "target_url": "/api/1/content/sha1_git:0b0ee9cc20323d3e4206eb3172f111bb211274e3/", - "type": "file" - }, - ], - "path": ".", - "revision": "ec72c666fb345ea5f21359b7bc063710ce558e39", - "type": "dir" - } - -.. http:get:: /api/1/revision/(sha1_git)[/prev/(prev_sha1s)]/log/ - - Get a list of all SWH revisions heading to a given one, i.e., show the commit log. - - :param string sha1_git: hexadecimal representation of the revision *sha1_git* identifier - :param string prev_sha1s: optional parameter representing the navigation breadcrumbs - (descendant revisions previously visited). If multiple values, use / as delimiter. - If provided, revisions information will be added at the beginning of the returned list. - :query int per_page: number of elements in the returned list, for pagination purpose - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - :resheader Link: indicates that a subsequent result page is available and contains - the url pointing to it - - **Response JSON Array of Objects:** - - array of revisions information as returned by :http:get:`/api/1/revision/(sha1_git)/` - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid *sha1_git* value has been provided - :statuscode 404: requested revision can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`revision/e1a315fa3fa734e2a6154ed7b5b9ae0eb8987aad/log/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - [ - { - "author": { - "email": "uwog@bcba8976-2d24-0410-9c9c-aab3bd5fdfd6", - "fullname": "uwog ", - "id": 1234212, - "name": "uwog" - }, - "author_url": "/api/1/person/1234212/", - "committer": { - "email": "uwog@bcba8976-2d24-0410-9c9c-aab3bd5fdfd6", - "fullname": "uwog ", - "id": 1234212, - "name": "uwog" - }, - "committer_date": "2010-10-09T21:28:27+00:00", - "committer_url": "/api/1/person/1234212/", - "date": "2010-10-09T21:28:27+00:00", - "directory": "d8f68b3628ac6f32b6532688bea3574c378ba403", - "directory_url": "/api/1/directory/d8f68b3628ac6f32b6532688bea3574c378ba403/", - "history_url": "/api/1/revision/e1a315fa3fa734e2a6154ed7b5b9ae0eb8987aad/log/", - "id": "e1a315fa3fa734e2a6154ed7b5b9ae0eb8987aad", - "merge": false, - "message": "Restore binary compatibility again, 1.2.8 broke it (Fridrich Strba)\n\n\n\ngit-svn-id: http://svn.abisource.com/wv/trunk@29360 bcba8976-2d24-0410-9c9c-aab3bd5fdfd6\n", - "metadata": {}, - "parents": [ - { - "id": "e9a1e0e2805d01095bccea37ddabae5b3853bf74", - "url": "/api/1/revision/e9a1e0e2805d01095bccea37ddabae5b3853bf74/" - } - ], - "synthetic": false, - "type": "git", - "url": "/api/1/revision/e1a315fa3fa734e2a6154ed7b5b9ae0eb8987aad/" - }, - { - "author": { - "email": "uwog@bcba8976-2d24-0410-9c9c-aab3bd5fdfd6", - "fullname": "uwog ", - "id": 1234212, - "name": "uwog" - }, - "author_url": "/api/1/person/1234212/", - "committer": { - "email": "uwog@bcba8976-2d24-0410-9c9c-aab3bd5fdfd6", - "fullname": "uwog ", - "id": 1234212, - "name": "uwog" - }, - "committer_date": "2010-10-09T10:42:20+00:00", - "committer_url": "/api/1/person/1234212/", - "date": "2010-10-09T10:42:20+00:00", - "directory": "00d3b261bb4e9253a84409cb7f69ed866fdbff5c", - "directory_url": "/api/1/directory/00d3b261bb4e9253a84409cb7f69ed866fdbff5c/", - "history_url": "/api/1/revision/e9a1e0e2805d01095bccea37ddabae5b3853bf74/log/", - "id": "e9a1e0e2805d01095bccea37ddabae5b3853bf74", - "merge": false, - "message": "Bump version\n\n\n\ngit-svn-id: http://svn.abisource.com/wv/trunk@29356 bcba8976-2d24-0410-9c9c-aab3bd5fdfd6\n", - "metadata": {}, - "parents": [ - { - "id": "73c589f19c060eb8af7c3fd3b4f50d44dd5218c8", - "url": "/api/1/revision/73c589f19c060eb8af7c3fd3b4f50d44dd5218c8/" - } - ], - "synthetic": false, - "type": "git", - "url": "/api/1/revision/e9a1e0e2805d01095bccea37ddabae5b3853bf74/" - }, - ] - - -.. http:get:: /api/1/revision/origin/(origin_id)/[branch/(branch_name)/][ts/(timestamp)/] - - Get information about a revision, searching for it based on software origin, - branch name, and/or visit timestamp. - - This endpoint behaves like :http:get:`/api/1/revision/(sha1_git)/`, - but operates on the revision that has been found at a given software origin, - close to a given point in time, pointed by a given branch. - - :param int origin_id: a SWH origin identifier - :param string branch_name: optional parameter specifying a fully-qualified branch name - associated to the software origin, e.g., "refs/heads/master". Defaults to the master branch. - :param string timestamp: optional parameter specifying a timestamp close to which the revision - pointed by the given branch should be looked up. The timestamp can be expressed either - as an ISO date or as a Unix one (in UTC). Defaults to now. - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - **Response JSON Object:** - same object as returned by :http:get:`/api/1/revision/(sha1_git)/` - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 404: no revision matching the given criteria could be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`revision/origin/13706355/branch/refs/heads/2.7/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "author": { - "email": "victor.stinner@gmail.com", - "fullname": "Victor Stinner ", - "id": 56592, - "name": "Victor Stinner" - }, - "author_url": "/api/1/person/56592/", - "committer": { - "email": "noreply@github.com", - "fullname": "GitHub ", - "id": 10932771, - "name": "GitHub" - }, - "committer_date": "2017-05-05T03:14:23+02:00", - "committer_url": "/api/1/person/10932771/", - "date": "2017-05-05T03:14:23+02:00", - "directory": "8657d73ae7c4018e24874f952d9b525bb6299027", - "directory_url": "/api/1/directory/8657d73ae7c4018e24874f952d9b525bb6299027/", - "history_url": "/api/1/revision/8a19eb24c97ef43e9fc7d45af180334ac8093545/log/", - "id": "8a19eb24c97ef43e9fc7d45af180334ac8093545", - "merge": false, - "message": "bpo-23404: make touch becomes make regen-all (#1466)\n\nDon't rebuild generated files based on file modification time\r\nanymore, the action is now explicit. Replace \"make touch\"\r\nwith \"make regen-all\".\r\n\r\nChanges:\r\n\r\n* Remove \"make touch\", Tools/hg/hgtouch.py and .hgtouch\r\n* Add a new \"make regen-all\" command to rebuild all generated files\r\n* Add subcommands to only generate specific files:\r\n\r\n - regen-ast: Include/Python-ast.h and Python/Python-ast.c\r\n - regen-grammar: Include/graminit.h and Python/graminit.c\r\n - regen-opcode-targets: Python/opcode_targets.h\r\n\r\n* Add PYTHON_FOR_REGEN variable\r\n* pgen is now only built by by \"make regen-grammar\"\r\n* Add $(srcdir)/ prefix to paths to source files to handle correctly\r\n compilation outside the source directory", - "metadata": {}, - "parents": [ - { - "id": "e81e355a8e43956802211115e3f99859a1a29ecb", - "url": "/api/1/revision/e81e355a8e43956802211115e3f99859a1a29ecb/" - } - ], - "synthetic": false, - "type": "git", - "url": "/api/1/revision/8a19eb24c97ef43e9fc7d45af180334ac8093545/" - } - -.. http:get:: /api/1/revision/origin/(origin_id)[/branch/(branch_name)][/ts/(timestamp)]/log - - Show the commit log for a revision, searching for it based on software origin, - branch name, and/or visit timestamp. - - This endpoint behaves like :http:get:`/api/1/revision/(sha1_git)[/prev/(prev_sha1s)]/log/`, - but operates on the revision that has been found at a given software origin, - close to a given point in time, pointed by a given branch. - - :param int origin_id: a SWH origin identifier - :param string branch_name: optional parameter specifying a fully-qualified branch name - associated to the software origin, e.g., "refs/heads/master". Defaults to the master branch. - :param string timestamp: optional parameter specifying a timestamp close to which the revision - pointed by the given branch should be looked up. The timestamp can be expressed either - as an ISO date or as a Unix one (in UTC). Defaults to now. - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - **Response JSON Array of Objects:** - - array of revisions information as returned by :http:get:`/api/1/revision/(sha1_git)/` - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 404: no revision matching the given criteria could be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`revision/origin/723566/ts/2016-01-17T00:00:00+00:00/log/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - [ - { - "author": { - "email": "gitster@pobox.com", - "fullname": "Junio C Hamano ", - "id": 4974, - "name": "Junio C Hamano" - }, - "author_url": "/api/1/person/4974/", - "committer": { - "email": "gitster@pobox.com", - "fullname": "Junio C Hamano ", - "id": 4974, - "name": "Junio C Hamano" - }, - "committer_date": "2016-02-24T13:31:57-08:00", - "committer_url": "/api/1/person/4974/", - "date": "2016-02-24T13:31:57-08:00", - "directory": "6985b8ccee00205572e706add0359e8f2b4c83b4", - "directory_url": "/api/1/directory/6985b8ccee00205572e706add0359e8f2b4c83b4/", - "history_url": "/api/1/revision/56f37fda511e1615dc6df86c68f3b841711a7828/log/", - "id": "56f37fda511e1615dc6df86c68f3b841711a7828", - "merge": false, - "message": "Eighth batch for 2.8\n\nSigned-off-by: Junio C Hamano \n", - "metadata": {}, - "parents": [ - { - "id": "c3b1e8d85133e2a19d372b7c166d5b49fcbbfef2", - "url": "/api/1/revision/c3b1e8d85133e2a19d372b7c166d5b49fcbbfef2/" - } - ], - "synthetic": false, - "type": "git", - "url": "/api/1/revision/56f37fda511e1615dc6df86c68f3b841711a7828/" - }, - { - "author": { - "email": "gitster@pobox.com", - "fullname": "Junio C Hamano ", - "id": 4974, - "name": "Junio C Hamano" - }, - "author_url": "/api/1/person/4974/", - "committer": { - "email": "gitster@pobox.com", - "fullname": "Junio C Hamano ", - "id": 4974, - "name": "Junio C Hamano" - }, - "committer_date": "2016-02-24T13:26:01-08:00", - "committer_url": "/api/1/person/4974/", - "date": "2016-02-24T13:26:01-08:00", - "directory": "0db4cfbd62218fbf54be4160420b6e9c67cd60a0", - "directory_url": "/api/1/directory/0db4cfbd62218fbf54be4160420b6e9c67cd60a0/", - "history_url": "/api/1/revision/c3b1e8d85133e2a19d372b7c166d5b49fcbbfef2/log/", - "id": "c3b1e8d85133e2a19d372b7c166d5b49fcbbfef2", - "merge": true, - "message": "Merge branch 'jc/am-i-v-fix'\n\nThe \"v(iew)\" subcommand of the interactive \"git am -i\" command was\nbroken in 2.6.0 timeframe when the command was rewritten in C.\n\n* jc/am-i-v-fix:\n am -i: fix \"v\"iew\n pager: factor out a helper to prepare a child process to run the pager\n pager: lose a separate argv[]\n", - "metadata": {}, - "parents": [ - { - "id": "595bfefa6c31fa6d76b686ed79b024838db5933e", - "url": "/api/1/revision/595bfefa6c31fa6d76b686ed79b024838db5933e/" - }, - { - "id": "708b8cc9a114ea1e5b90f5f52fd24ecade4e8b40", - "url": "/api/1/revision/708b8cc9a114ea1e5b90f5f52fd24ecade4e8b40/" - } - ], - "synthetic": false, - "type": "git", - "url": "/api/1/revision/c3b1e8d85133e2a19d372b7c166d5b49fcbbfef2/" - }, - ] +.. autosimple:: swh.web.api.views.revision.api_revision_with_origin +.. autosimple:: swh.web.api.views.revision.api_revision_log_by diff --git a/docs/uri-scheme-api-snapshot.rst b/docs/uri-scheme-api-snapshot.rst index ea2e4634..83085ef0 100644 --- a/docs/uri-scheme-api-snapshot.rst +++ b/docs/uri-scheme-api-snapshot.rst @@ -1,76 +1,4 @@ Snapshot -------- -.. http:get:: /api/1/snapshot/(snapshot_id)/ - - Get information about a snapshot in the SWH archive. - - A snapshot is a set of named branches, which are pointers to objects at any - level of the Software Heritage DAG. It represents a full picture of an - origin at a given time. - - As well as pointing to other objects in the Software Heritage DAG, branches - can also be aliases, in which case their target is the name of another - branch in the same snapshot, or dangling, in which case the target is - unknown. - - A snapshot identifier is a salted sha1. See :func:`swh.model.identifiers.snapshot_identifier` - in our data model module for details about how they are computed. - - :param sha1 snapshot_id: a SWH snapshot identifier - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - :>json object branches: object containing all branches associated to the snapshot, - for each of them the associated SWH target type and id are given but also - a link to get information about that target - :>json string id: the unique identifier of the snapshot - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - :statuscode 400: an invalid snapshot identifier has been provided - :statuscode 404: requested snapshot can not be found in the SWH archive - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`snapshot/6a3a2cf0b2b90ce7ae1cf0a221ed68035b686f5a/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "branches": { - "refs/heads/master": { - "target": "83c20a6a63a7ebc1a549d367bc07a61b926cecf3", - "target_type": "revision", - "target_url": "/api/1/revision/83c20a6a63a7ebc1a549d367bc07a61b926cecf3/" - }, - "refs/heads/wiki": { - "target": "71f667aeb5d02562f2fa0941ad91df69c474ff3b", - "target_type": "revision", - "target_url": "/api/1/revision/71f667aeb5d02562f2fa0941ad91df69c474ff3b/" - }, - "refs/tags/dpkt-1.6": { - "target": "7fc0fd582812af36064d1c85fe51e33227920479", - "target_type": "revision", - "target_url": "/api/1/revision/7fc0fd582812af36064d1c85fe51e33227920479/" - }, - "refs/tags/dpkt-1.7": { - "target": "0c9dbfbc0974ec8ac1d8253aa1092366a03633a8", - "target_type": "revision", - "target_url": "/api/1/revision/0c9dbfbc0974ec8ac1d8253aa1092366a03633a8/" - } - }, - "id": "6a3a2cf0b2b90ce7ae1cf0a221ed68035b686f5a" - } - - +.. autosimple:: swh.web.api.views.snapshot.api_snapshot diff --git a/docs/uri-scheme-api-stat.rst b/docs/uri-scheme-api-stat.rst index 09e4521d..d9a42903 100644 --- a/docs/uri-scheme-api-stat.rst +++ b/docs/uri-scheme-api-stat.rst @@ -1,67 +1,4 @@ Archive statistics ------------------ -.. http:get:: /api/1/stat/counters - - Get statistics about the content of the archive. - - :>json number content: current number of content objects (aka files) in the SWH archive - :>json number directory: current number of directory objects in the SWH archive - :>json number directory_entry_dir: current number of SWH directory entries - pointing to others SWH directories in the SWH archive - :>json number directory_entry_file: current number of SWH directory entries - pointing to SWH content objects in the SWH archive - :>json number directory_entry_rev: current number of SWH directory entries - pointing to SWH revision objects (e.g. git submodules) in the SWH archive - :>json number entity: current number of SWH entities (a SWH entity is either - a *group_of_entities*, a *group_of_persons*, a *project*, a *person*, an *organization*, - or a *hosting* service) in the SWH archive - :>json number occurrence: current number of SWH occurrences (an occurrence may be assimilated - to a branch found during a SWH crawl of a repository) in the SWH archive - :>json number origin: current number of SWH origins (an origin is a "place" where code - source can be found, e.g. a git repository, a tarball, ...) in the SWH archive - :>json number person: current number of SWH persons (code source authors or commiters) - in the SWH archive - :>json number release: current number of SWH releases objects in the SWH archive - :>json number revision: current number of SWH revision objects (aka commits) in the SWH archive - :>json number skipped_content: current number of content objects (aka files) which where - not inserted in the SWH archive - - :reqheader Accept: the requested response content type, - either *application/json* (default) or *application/yaml* - :resheader Content-Type: this depends on :http:header:`Accept` header of request - - **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` - - :statuscode 200: no error - - **Request:** - - .. parsed-literal:: - - $ curl -i :swh_web_api:`stat/counters/` - - **Response:** - - .. sourcecode:: http - - HTTP/1.1 200 OK - Content-Type: application/json - - { - "content": 3837301367, - "directory": 3385342732, - "directory_entry_dir": 2696063206, - "directory_entry_file": 3969668591, - "directory_entry_rev": 8201450, - "entity": 7101551, - "entity_history": 7148571, - "occurrence": 538691292, - "occurrence_history": 554791126, - "origin": 65642044, - "person": 17211993, - "release": 6666960, - "revision": 879725958, - "revision_history": 908684112, - "skipped_content": 19383 - } +.. autosimple:: swh.web.api.views.stat.api_stats diff --git a/docs/uri-scheme-api-vault.rst b/docs/uri-scheme-api-vault.rst new file mode 100644 index 00000000..765e7904 --- /dev/null +++ b/docs/uri-scheme-api-vault.rst @@ -0,0 +1,10 @@ +Vault +----- + +.. autosimple:: swh.web.api.views.vault.api_vault_cook_directory + +.. autosimple:: swh.web.api.views.vault.api_vault_fetch_directory + +.. autosimple:: swh.web.api.views.vault.api_vault_cook_revision_gitfast + +.. autosimple:: swh.web.api.views.vault.api_vault_fetch_revision_gitfast diff --git a/docs/uri-scheme-api.rst b/docs/uri-scheme-api.rst index c64375cd..6d0dde20 100644 --- a/docs/uri-scheme-api.rst +++ b/docs/uri-scheme-api.rst @@ -1,20 +1,22 @@ .. _swh-web-api-urls: SWH Web API URLs ================ .. include:: uri-scheme-api-content.rst .. include:: uri-scheme-api-directory.rst .. include:: uri-scheme-api-origin.rst .. include:: uri-scheme-api-person.rst .. include:: uri-scheme-api-release.rst .. include:: uri-scheme-api-revision.rst .. include:: uri-scheme-api-snapshot.rst .. include:: uri-scheme-api-stat.rst + +.. include:: uri-scheme-api-vault.rst diff --git a/swh/web/api/apidoc.py b/swh/web/api/apidoc.py index 6895b079..3f7a39df 100644 --- a/swh/web/api/apidoc.py +++ b/swh/web/api/apidoc.py @@ -1,314 +1,346 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +import docutils.nodes +import docutils.parsers.rst +import docutils.utils +import functools +import os import re -from collections import defaultdict from functools import wraps -from enum import Enum - from rest_framework.decorators import api_view -from swh.web.common.utils import reverse +from swh.web.common.utils import parse_rst from swh.web.api.apiurls import APIUrls from swh.web.api.apiresponse import make_api_response, error_response -class argtypes(Enum): # noqa: N801 - """Class for centralizing argument type descriptions - +class _HTTPDomainDocVisitor(docutils.nodes.NodeVisitor): + """ + docutils visitor for walking on a parsed rst document containing sphinx + httpdomain roles. Its purpose is to extract relevant info regarding swh + api endpoints (for instance url arguments) from their docstring written + using sphinx httpdomain. """ - ts = 'timestamp' - int = 'integer' - str = 'string' - path = 'path' - sha1 = 'sha1' - uuid = 'uuid' - sha1_git = 'sha1_git' - algo_and_hash = 'hash_type:hash' + # httpdomain roles we want to parse (based on sphinxcontrib.httpdomain 1.6) + parameter_roles = ('param', 'parameter', 'arg', 'argument') + response_json_object_roles = ('resjsonobj', 'resjson', '>jsonobj', '>json') -class rettypes(Enum): # noqa: N801 - """Class for centralizing return type descriptions + response_json_array_roles = ('resjsonarr', '>jsonarr') - """ - octet_stream = 'octet stream' - list = 'list' - dict = 'dict' + query_parameter_roles = ('queryparameter', 'queryparam', 'qparam', 'query') + request_header_roles = ('header', 'resheader', 'responseheader') - """ + status_code_roles = ('statuscode', 'status', 'code') + + def __init__(self, document, urls, data): + super().__init__(document) + self.urls = urls + self.url_idx = 0 + self.data = data + self.args_set = set() + self.params_set = set() + self.returns_set = set() + self.status_codes_set = set() + self.reqheaders_set = set() + self.resheaders_set = set() + self.field_list_visited = False - badinput = 'BadInputExc' - notfound = 'NotFoundExc' + def process_paragraph(self, par): + """ + Process extracted paragraph text before display. + Cleanup document model markups and transform the + paragraph into a valid raw rst string (as the apidoc + documentation transform rst to html when rendering). + """ + par = par.replace('\n', ' ') + # keep empahasized and strong text + par = par.replace('', '*') + par = par.replace('', '*') + par = par.replace('', '**') + par = par.replace('', '**') + # remove parsed document markups + par = re.sub('<[^<]+?>', '', par) + # api urls cleanup to generate valid links afterwards + par = re.sub('\(\w+\)', '', par) + par = re.sub('\[.*\]', '', par) + par = par.replace('//', '/') + # transform references to api endpoints into valid rst links + par = re.sub(':http:get:`(.*)`', r'`<\1>`_', par) + # transform references to some elements into bold text + par = re.sub(':http:header:`(.*)`', r'**\1**', par) + par = re.sub(':func:`(.*)`', r'**\1**', par) + return par + + def visit_field_list(self, node): + """ + Visit parsed rst field lists to extract relevant info + regarding api endpoint. + """ + self.field_list_visited = True + for child in node.traverse(): + # get the parsed field name + if isinstance(child, docutils.nodes.field_name): + field_name = child.astext() + # parse field text + elif isinstance(child, docutils.nodes.paragraph): + text = self.process_paragraph(str(child)) + field_data = field_name.split(' ') + # Parameters + if field_data[0] in self.parameter_roles: + if field_data[2] not in self.args_set: + self.data['args'].append({'name': field_data[2], + 'type': field_data[1], + 'doc': text}) + self.args_set.add(field_data[2]) + # Query Parameters + if field_data[0] in self.query_parameter_roles: + if field_data[2] not in self.params_set: + self.data['params'].append({'name': field_data[2], + 'type': field_data[1], + 'doc': text}) + self.params_set.add(field_data[2]) + # Response type + if field_data[0] in self.response_json_array_roles or \ + field_data[0] in self.response_json_object_roles: + # array + if field_data[0] in self.response_json_array_roles: + self.data['return_type'] = 'array' + # object + else: + self.data['return_type'] = 'object' + # returned object field + if field_data[2] not in self.returns_set: + self.data['returns'].append({'name': field_data[2], + 'type': field_data[1], + 'doc': text}) + self.returns_set.add(field_data[2]) + # Status Codes + if field_data[0] in self.status_code_roles: + if field_data[1] not in self.status_codes_set: + self.data['status_codes'].append({'code': field_data[1], # noqa + 'doc': text}) + self.status_codes_set.add(field_data[1]) + # Request Headers + if field_data[0] in self.request_header_roles: + if field_data[1] not in self.reqheaders_set: + self.data['reqheaders'].append({'name': field_data[1], + 'doc': text}) + self.reqheaders_set.add(field_data[1]) + # Response Headers + if field_data[0] in self.response_header_roles: + if field_data[1] not in self.resheaders_set: + resheader = {'name': field_data[1], + 'doc': text} + self.data['resheaders'].append(resheader) + self.resheaders_set.add(field_data[1]) + if resheader['name'] == 'Content-Type' and \ + resheader['doc'] == 'application/octet-stream': + self.data['return_type'] = 'octet stream' + + def visit_paragraph(self, node): + """ + Visit relevant paragraphs to parse + """ + # only parsed top level paragraphs + if isinstance(node.parent, docutils.nodes.block_quote): + text = self.process_paragraph(str(node)) + # endpoint description + if not text.startswith('**'): + self.data['description'] += '\n\n' if self.data['description'] else '' # noqa + self.data['description'] += text + # http methods + elif text.startswith('**Allowed HTTP Methods:**'): + text = text.replace('**Allowed HTTP Methods:**', '') + http_methods = text.strip().split(',') + http_methods = [m[m.find('`')+1:-1].upper() + for m in http_methods] + self.data['urls'].append({'rule': self.urls[self.url_idx], + 'methods': http_methods}) + self.url_idx += 1 + + def visit_literal_block(self, node): + """ + Visit litteral blocks + """ + text = node.astext() + # litteral block in endpoint description + if not self.field_list_visited: + self.data['description'] += ':\n\n\t%s' % text + # extract example url + if ':swh_web_api:' in text: + self.data['examples'].append( + '/api/1/' + re.sub('.*`(.*)`.*', r'\1', text)) + + def unknown_visit(self, node): + pass + + def depart_document(self, node): + """ + End of parsing extra processing + """ + default_methods = ['GET', 'HEAD', 'OPTIONS'] + # ensure urls info is present and set default http methods + if not self.data['urls']: + for url in self.urls: + self.data['urls'].append({'rule': url, + 'methods': default_methods}) + + def unknown_departure(self, node): + pass + + +def _parse_httpdomain_doc(doc, data): + doc_lines = doc.split('\n') + doc_lines_filtered = [] + urls = [] + # httpdomain is a sphinx extension that is unknown to docutils but + # fortunately we can still parse its directives' content, + # so remove lines with httpdomain directives before executing the + # rst parser from docutils + for doc_line in doc_lines: + if '.. http' not in doc_line: + doc_lines_filtered.append(doc_line) + else: + url = doc_line[doc_line.find('/'):] + # emphasize url arguments for html rendering + url = re.sub(r'\((\w+)\)', r' **\(\1\)** ', url) + urls.append(url) + # parse the rst doctring and do not print system messages about + # unknown httpdomain roles + document = parse_rst('\n'.join(doc_lines_filtered), report_level=5) + # remove the system_message nodes from the parsed document + for node in document.traverse(docutils.nodes.system_message): + node.parent.remove(node) + # visit the document nodes to extract relevant endpoint info + visitor = _HTTPDomainDocVisitor(document, urls, data) + document.walkabout(visitor) class APIDocException(Exception): """ Custom exception to signal errors in the use of the APIDoc decorators """ -class route(object): # noqa: N801 - """Decorate an API method to register it in the API doc route index - and create the corresponding Flask route. - - This decorator is responsible for bootstrapping the linking of subsequent - decorators, as well as traversing the decorator stack to obtain the - documentation data from it. +class api_doc(object): # noqa: N801 + """ + Decorate an API function to register it in the API doc route index + and create the corresponding DRF route. Args: - route: documentation page's route - noargs: set to True if the route has no arguments, and its - result should be displayed anytime its documentation - is requested. Default to False - hidden: set to True to remove the endpoint from being listed - in the /api endpoints. Default to False. - tags: Further information on api endpoints. Two values are - possibly expected: - - hidden: remove the entry points from the listing - - upcoming: display the entry point but it is not followable + route (str): documentation page's route + noargs (boolean): set to True if the route has no arguments, and its + result should be displayed anytime its documentation + is requested. Default to False + tags (list): Further information on api endpoints. Two values are + possibly expected: + + * hidden: remove the entry points from the listing + * upcoming: display the entry point but it is not followable + + handle_response (boolean): indicate if the decorated function takes + care of creating the HTTP response or delegates that task to the + apiresponse module + api_version (str): api version string """ def __init__(self, route, noargs=False, tags=[], handle_response=False, api_version='1'): super().__init__() self.route = route self.urlpattern = '^' + api_version + route + '$' self.noargs = noargs self.tags = set(tags) self.handle_response = handle_response - # @apidoc.route() Decorator call + # @api_doc() Decorator call def __call__(self, f): + # If the route is not hidden, add it to the index if 'hidden' not in self.tags: - APIUrls.add_route(self.route, f.__doc__, tags=self.tags) + doc_data = self.get_doc_data(f) + doc_desc = doc_data['description'] + first_dot_pos = doc_desc.find('.') + APIUrls.add_route(self.route, doc_desc[:first_dot_pos+1], + tags=self.tags) # If the decorated route has arguments, we create a specific # documentation view if not self.noargs: @api_view(['GET', 'HEAD']) def doc_view(request): doc_data = self.get_doc_data(f) return make_api_response(request, None, doc_data) view_name = self.route[1:-1].replace('/', '-') APIUrls.add_url_pattern(self.urlpattern, doc_view, view_name) @wraps(f) def documented_view(request, **kwargs): doc_data = self.get_doc_data(f) try: - rv = f(request, **kwargs) + response = f(request, **kwargs) except Exception as exc: return error_response(request, exc, doc_data) if self.handle_response: - return rv + return response else: - return make_api_response(request, rv, doc_data) + return make_api_response(request, response, doc_data) return documented_view - def filter_api_url(self, endpoint, route_re, noargs): - doc_methods = {'GET', 'HEAD', 'OPTIONS'} - if re.match(route_re, endpoint['rule']): - if endpoint['methods'] == doc_methods and not noargs: - return False - return True - - def build_examples(self, urls, args): - """Build example documentation. - - Args: - f: function - urls: information relative to url for that function - args: information relative to arguments for that function - - Yields: - example based on default parameter value if any - - """ - s = set() - r = [] - for data_url in urls: - url = data_url['rule'] - defaults = {arg['name']: arg['default'] - for arg in args - if arg['name'] in url} - if defaults and None not in defaults.values(): - url = reverse(data_url['name'], kwargs=defaults) - if url in s: - continue - s.add(url) - r.append(url) - - return r - + @functools.lru_cache(maxsize=32) def get_doc_data(self, f): - """Build documentation data for the decorated function""" + """ + Build documentation data for the decorated api endpoint function + """ data = { - 'docstring': None, + 'description': '', 'response_data': None, - 'urls': None, - 'args': None, - 'params': None, - 'headers': None, - 'returns': None, - 'excs': None, - 'examples': None, + 'urls': [], + 'args': [], + 'params': [], + 'resheaders': [], + 'reqheaders': [], + 'return_type': '', + 'returns': [], + 'status_codes': [], + 'examples': [], 'route': self.route, 'noargs': self.noargs } - data.update(getattr(f, 'doc_data', {})) - if not f.__doc__: - raise APIDocException('Apidoc %s: expected a docstring' + raise APIDocException('apidoc %s: expected a docstring' ' for function %s' % (self.__class__.__name__, f.__name__)) - data['docstring'] = f.__doc__ - - route_re = re.compile('.*%s$' % data['route']) - endpoint_list = APIUrls.get_method_endpoints(f) - data['urls'] = [url for url in endpoint_list if - self.filter_api_url(url, route_re, data['noargs'])] - - if data['args']: - data['examples'] = self.build_examples(data['urls'], data['args']) - data['heading'] = '%s Documentation' % data['route'] + # use raw docstring as endpoint documentation if sphinx + # httpdomain is not used + if '.. http' not in f.__doc__: + data['description'] = f.__doc__ + # else parse the sphinx httpdomain docstring with docutils + # (except when building the swh-web documentation through autodoc + # sphinx extension, not needed and raise errors with sphinx >= 1.7) + elif 'SWH_WEB_DOC_BUILD' not in os.environ: + _parse_httpdomain_doc(f.__doc__, data) + # process returned object info for nicer html display + returns_list = '' + for ret in data['returns']: + returns_list += '\t* **%s (%s)**: %s\n' %\ + (ret['name'], ret['type'], ret['doc']) + data['returns_list'] = returns_list return data - - -class DocData(object): - """Base description of optional input/output setup for a route. - - """ - destination = None - - def __init__(self): - self.doc_data = {} - - def __call__(self, f): - if not hasattr(f, 'doc_data'): - f.doc_data = defaultdict(list) - - f.doc_data[self.destination].append(self.doc_data) - - return f - - -class arg(DocData): # noqa: N801 - """ - Decorate an API method to display an argument's information on the doc - page specified by @route above. - - Args: - name: the argument's name. MUST match the method argument's name to - create the example request URL. - default: the argument's default value - argtype: the argument's type as an Enum value from apidoc.argtypes - argdoc: the argument's documentation string - """ - destination = 'args' - - def __init__(self, name, default, argtype, argdoc): - super().__init__() - self.doc_data = { - 'name': name, - 'type': argtype.value, - 'doc': argdoc, - 'default': default - } - - -class header(DocData): # noqa: N801 - """ - Decorate an API method to display header information the api can - potentially return in the response. - - Args: - name: the header name - doc: the information about that header - - """ - destination = 'headers' - - def __init__(self, name, doc): - super().__init__() - self.doc_data = { - 'name': name, - 'doc': doc, - } - - -class param(DocData): # noqa: N801 - """Decorate an API method to display query parameter information the - api can potentially accept. - - Args: - name: parameter's name - default: parameter's default value - argtype: parameter's type as an Enum value from apidoc.argtypes - doc: the information about that header - - """ - destination = 'params' - - def __init__(self, name, default, argtype, doc): - super().__init__() - self.doc_data = { - 'name': name, - 'type': argtype.value, - 'default': default, - 'doc': doc, - } - - -class raises(DocData): # noqa: N801 - """Decorate an API method to display information pertaining to an exception - that can be raised by this method. - - Args: - exc: the exception name as an Enum value from apidoc.excs - doc: the exception's documentation string - - """ - destination = 'excs' - - def __init__(self, exc, doc): - super().__init__() - self.doc_data = { - 'exc': exc.value, - 'doc': doc - } - - -class returns(DocData): # noqa: N801 - """Decorate an API method to display information about its return value. - - Args: - rettype: the return value's type as an Enum value from - apidoc.rettypes retdoc: the return value's documentation - string - - """ - destination = 'returns' - - def __init__(self, rettype=None, retdoc=None): - super().__init__() - self.doc_data = { - 'type': rettype.value, - 'doc': retdoc - } diff --git a/swh/web/api/apiurls.py b/swh/web/api/apiurls.py index 45958690..1a3f1a90 100644 --- a/swh/web/api/apiurls.py +++ b/swh/web/api/apiurls.py @@ -1,126 +1,79 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information -import re - from rest_framework.decorators import api_view from swh.web.common.urlsindex import UrlsIndex from swh.web.common.throttling import throttle_scope class APIUrls(UrlsIndex): """ Class to manage API documentation URLs. - Indexes all routes documented using apidoc's decorators. - Tracks endpoint/request processing method relationships for use in generating related urls in API documentation """ _apidoc_routes = {} _method_endpoints = {} scope = 'api' @classmethod def get_app_endpoints(cls): return cls._apidoc_routes - @classmethod - def get_method_endpoints(cls, f): - if f.__name__ not in cls._method_endpoints: - cls._method_endpoints[f.__name__] = cls.group_routes_by_method(f) - return cls._method_endpoints[f.__name__] - - @classmethod - def group_routes_by_method(cls, f): - """ - Group URL endpoints according to their processing method. - - Returns: - A dict where keys are the processing method names, and values are - the routes that are bound to the key method. - - """ - rules = [] - for urlp in cls.get_url_patterns(): - endpoint = urlp.callback.__name__ - if endpoint != f.__name__: - continue - method_names = urlp.callback.http_method_names - url_rule = urlp.regex.pattern.replace('^', '/').replace('$', '') - url_rule_params = re.findall('\([^)]+\)', url_rule) - for param in url_rule_params: - param_name = re.findall('<(.*)>', param) - param_name = param_name[0] if len(param_name) > 0 else None - if param_name and hasattr(f, 'doc_data') and f.doc_data['args']: # noqa - param_index = \ - next(i for (i, d) in enumerate(f.doc_data['args']) - if d['name'] == param_name) - if param_index is not None: - url_rule = url_rule.replace( - param, '<' + - f.doc_data['args'][param_index]['name'] + - ': ' + f.doc_data['args'][param_index]['type'] + - '>').replace('.*', '') - rule_dict = {'rule': '/api' + url_rule, - 'name': urlp.name, - 'methods': {method.upper() for method in method_names} - } - rules.append(rule_dict) - - return rules - @classmethod def add_route(cls, route, docstring, **kwargs): """ Add a route to the self-documenting API reference """ route_view_name = route[1:-1].replace('/', '-') if route not in cls._apidoc_routes: d = {'docstring': docstring, 'route_view_name': route_view_name} for k, v in kwargs.items(): d[k] = v cls._apidoc_routes[route] = d class api_route(object): # noqa: N801 """ Decorator to ease the registration of an API endpoint using the Django REST Framework. Args: url_pattern: the url pattern used by DRF to identify the API route view_name: the name of the API view associated to the route used to reverse the url methods: array of HTTP methods supported by the API route """ def __init__(self, url_pattern=None, view_name=None, methods=['GET', 'HEAD', 'OPTIONS'], throttle_scope='swh_api', api_version='1'): super().__init__() self.url_pattern = '^' + api_version + url_pattern + '$' self.view_name = view_name self.methods = methods self.throttle_scope = throttle_scope def __call__(self, f): # create a DRF view from the wrapped function @api_view(self.methods) @throttle_scope(self.throttle_scope) def api_view_f(*args, **kwargs): return f(*args, **kwargs) # small hacks for correctly generating API endpoints index doc api_view_f.__name__ = f.__name__ api_view_f.http_method_names = self.methods # register the route and its view in the endpoints index APIUrls.add_url_pattern(self.url_pattern, api_view_f, self.view_name) return f diff --git a/swh/web/api/views/content.py b/swh/web/api/views/content.py index 6769f66b..7cd775ab 100644 --- a/swh/web/api/views/content.py +++ b/swh/web/api/views/content.py @@ -1,340 +1,411 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information import functools from django.http import HttpResponse from swh.web.common import service from swh.web.common.utils import reverse from swh.web.common.exc import NotFoundExc, ForbiddenExc -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api import utils from swh.web.api.apiurls import api_route -from swh.web.api.views.utils import ( - api_lookup, doc_exc_id_not_found, doc_header_link, - doc_arg_last_elt, doc_arg_per_page, doc_exc_bad_id, - doc_arg_content_id -) +from swh.web.api.views.utils import api_lookup @api_route(r'/content/(?P.+)/provenance/', 'content-provenance') -@api_doc.route('/content/provenance/', tags=['hidden']) -@api_doc.arg('q', - default='sha1_git:88b9b366facda0b5ff8d8640ee9279bed346f242', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""List of provenance information (dict) for the matched - content.""") +@api_doc('/content/provenance/', tags=['hidden']) def api_content_provenance(request, q): """Return content's provenance information if any. """ def _enrich_revision(provenance): p = provenance.copy() p['revision_url'] = \ reverse('revision', kwargs={'sha1_git': provenance['revision']}) p['content_url'] = \ reverse('content', kwargs={'q': 'sha1_git:%s' % provenance['content']}) p['origin_url'] = \ reverse('origin', kwargs={'origin_id': provenance['origin']}) p['origin_visits_url'] = \ reverse('origin-visits', kwargs={'origin_id': provenance['origin']}) p['origin_visit_url'] = \ reverse('origin-visit', kwargs={'origin_id': provenance['origin'], 'visit_id': provenance['visit']}) return p return api_lookup( service.lookup_content_provenance, q, notfound_msg='Content with {} not found.'.format(q), enrich_fn=_enrich_revision) @api_route(r'/content/(?P.+)/filetype/', 'content-filetype') -@api_doc.route('/content/filetype/') -@api_doc.arg('q', - default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""Filetype information (dict) for the matched - content.""") +@api_doc('/content/filetype/') def api_content_filetype(request, q): - """Get information about the detected MIME type of a content object. - """ + .. http:get:: /api/1/content/[(hash_type):](hash)/filetype/ + + Get information about the detected MIME type of a content object. + + :param string hash_type: optional parameter specifying which hashing algorithm has been used + to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* + or *blake2s256*. If that parameter is not provided, it is assumed that the + hashing algorithm used is *sha1*. + :param string hash: hexadecimal representation of the checksum value computed with + the specified hashing algorithm. + + :>json object content_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/` for + getting information about the content + :>json string encoding: the detected content encoding + :>json string id: the *sha1* identifier of the content + :>json string mimetype: the detected MIME type of the content + :>json object tool: information about the tool used to detect the content filetype + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *hash_type* or *hash* has been provided + :statuscode 404: requested content can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/filetype/` + """ # noqa return api_lookup( service.lookup_content_filetype, q, notfound_msg='No filetype information found for content {}.'.format(q), enrich_fn=utils.enrich_metadata_endpoint) @api_route(r'/content/(?P.+)/language/', 'content-language') -@api_doc.route('/content/language/') -@api_doc.arg('q', - default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""Language information (dict) for the matched - content.""") +@api_doc('/content/language/') def api_content_language(request, q): - """Get information about the detected (programming) language of a content - object. - """ + .. http:get:: /api/1/content/[(hash_type):](hash)/language/ + + Get information about the programming language used in a content object. + + :param string hash_type: optional parameter specifying which hashing algorithm has been used + to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* + or *blake2s256*. If that parameter is not provided, it is assumed that the + hashing algorithm used is *sha1*. + :param string hash: hexadecimal representation of the checksum value computed with + the specified hashing algorithm. + + :>json object content_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/` for + getting information about the content + :>json string id: the *sha1* identifier of the content + :>json string lang: the detected programming language if any + :>json object tool: information about the tool used to detect the programming language + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *hash_type* or *hash* has been provided + :statuscode 404: requested content can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/language/` + """ # noqa return api_lookup( service.lookup_content_language, q, notfound_msg='No language information found for content {}.'.format(q), enrich_fn=utils.enrich_metadata_endpoint) @api_route(r'/content/(?P.+)/license/', 'content-license') -@api_doc.route('/content/license/') -@api_doc.arg('q', - default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""License information (dict) for the matched - content.""") +@api_doc('/content/license/') def api_content_license(request, q): - """Get information about the detected license of a content object. - """ + .. http:get:: /api/1/content/[(hash_type):](hash)/license/ + + Get information about the license of a content object. + + :param string hash_type: optional parameter specifying which hashing algorithm has been used + to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* + or *blake2s256*. If that parameter is not provided, it is assumed that the + hashing algorithm used is *sha1*. + :param string hash: hexadecimal representation of the checksum value computed with + the specified hashing algorithm. + + :>json object content_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/` for + getting information about the content + :>json string id: the *sha1* identifier of the content + :>json array licenses: array of strings containing the detected license names if any + :>json object tool: information about the tool used to detect the license + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *hash_type* or *hash* has been provided + :statuscode 404: requested content can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/license/` + """ # noqa return api_lookup( service.lookup_content_license, q, notfound_msg='No license information found for content {}.'.format(q), enrich_fn=utils.enrich_metadata_endpoint) @api_route(r'/content/(?P.+)/ctags/', 'content-ctags') -@api_doc.route('/content/ctags/', tags=['upcoming']) -@api_doc.arg('q', - default='sha1:1fc6129a692e7a87b5450e2ba56e7669d0c5775d', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""Ctags symbol (dict) for the matched - content.""") +@api_doc('/content/ctags/', tags=['hidden']) def api_content_ctags(request, q): - """Get information about all `Ctags `_-style + """ + Get information about all `Ctags `_-style symbols defined in a content object. - """ return api_lookup( service.lookup_content_ctags, q, notfound_msg='No ctags symbol found for content {}.'.format(q), enrich_fn=utils.enrich_metadata_endpoint) @api_route(r'/content/(?P.+)/raw/', 'content-raw') -@api_doc.route('/content/raw/', handle_response=True) -@api_doc.arg('q', - default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=doc_arg_content_id) -@api_doc.param('filename', default=None, - argtype=api_doc.argtypes.str, - doc='User\'s desired filename. If provided, the downloaded' - ' content will get that filename.') -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.octet_stream, - retdoc='The raw content data as an octet stream') +@api_doc('/content/raw/', handle_response=True) def api_content_raw(request, q): - """Get the raw content of a content object (AKA "blob"), as a byte - sequence. - """ + .. http:get:: /api/1/content/[(hash_type):](hash)/raw/ + + Get the raw content of a content object (aka a "blob"), as a byte sequence. + + :param string hash_type: optional parameter specifying which hashing algorithm has been used + to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* + or *blake2s256*. If that parameter is not provided, it is assumed that the + hashing algorithm used is *sha1*. + :param string hash: hexadecimal representation of the checksum value computed with + the specified hashing algorithm. + :query string filename: if provided, the downloaded content will get that filename + + :resheader Content-Type: application/octet-stream + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *hash_type* or *hash* has been provided + :statuscode 404: requested content can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`content/sha1:dc2830a9e72f23c1dfebef4413003221baa5fb62/raw/` + """ # noqa def generate(content): yield content['data'] content_raw = service.lookup_content_raw(q) if not content_raw: raise NotFoundExc('Content %s is not found.' % q) content_filetype = service.lookup_content_filetype(q) if not content_filetype: raise NotFoundExc('Content %s is not available for download.' % q) mimetype = content_filetype['mimetype'] if 'text/' not in mimetype: raise ForbiddenExc('Only textual content is available for download. ' 'Actual content mimetype is %s.' % mimetype) filename = request.query_params.get('filename') if not filename: filename = 'content_%s_raw' % q.replace(':', '_') response = HttpResponse(generate(content_raw), content_type='application/octet-stream') response['Content-disposition'] = 'attachment; filename=%s' % filename return response @api_route(r'/content/symbol/(?P.+)/', 'content-symbol') -@api_doc.route('/content/symbol/', tags=['upcoming']) -@api_doc.arg('q', - default='hello', - argtype=api_doc.argtypes.str, - argdoc="""An expression string to lookup in swh's raw content""") -@api_doc.header('Link', doc=doc_header_link) -@api_doc.param('last_sha1', default=None, - argtype=api_doc.argtypes.str, - doc=doc_arg_last_elt) -@api_doc.param('per_page', default=10, - argtype=api_doc.argtypes.int, - doc=doc_arg_per_page) -@api_doc.returns(rettype=api_doc.rettypes.list, - retdoc="""A list of dict whose content matches the expression. - Each dict has the following keys: - - id (bytes): identifier of the content - - name (text): symbol whose content match the expression - - kind (text): kind of the symbol that matched - - lang (text): Language for that entry - - line (int): Number line for the symbol - """) +@api_doc('/content/symbol/', tags=['hidden']) def api_content_symbol(request, q=None): """Search content objects by `Ctags `_-style symbol (e.g., function name, data type, method, ...). """ result = {} last_sha1 = request.query_params.get('last_sha1', None) per_page = int(request.query_params.get('per_page', '10')) def lookup_exp(exp, last_sha1=last_sha1, per_page=per_page): return service.lookup_expression(exp, last_sha1, per_page) symbols = api_lookup( lookup_exp, q, notfound_msg="No indexed raw content match expression '{}'.".format(q), enrich_fn=functools.partial(utils.enrich_content, top_url=True)) if symbols: nb_symbols = len(symbols) if nb_symbols == per_page: query_params = {} new_last_sha1 = symbols[-1]['sha1'] query_params['last_sha1'] = new_last_sha1 if request.query_params.get('per_page'): query_params['per_page'] = per_page result['headers'] = { 'link-next': reverse('content-symbol', kwargs={'q': q}, query_params=query_params) } result.update({ 'results': symbols }) return result @api_route(r'/content/known/search/', 'content-known', methods=['POST']) @api_route(r'/content/known/(?P(?!search).*)/', 'content-known') -@api_doc.route('/content/known/', tags=['hidden']) -@api_doc.arg('q', - default='adc83b19e793491b1c6ea0fd8b46cd9f32e592fc', - argtype=api_doc.argtypes.sha1, - argdoc='content identifier as a sha1 checksum') -@api_doc.param('q', default=None, - argtype=api_doc.argtypes.str, - doc="""(POST request) An algo_hash:hash string, where algo_hash - is one of sha1, sha1_git or sha256 and hash is the hash to - search for in SWH""") -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""a dictionary with results (found/not found for each given - identifier) and statistics about how many identifiers - were found""") +@api_doc('/content/known/', tags=['hidden']) def api_check_content_known(request, q=None): - """Check whether some content (AKA "blob") is present in the archive. + """ + .. http:get:: /api/1/content/known/(sha1)[,(sha1), ...,(sha1)]/ - Lookup can be performed by various means: + Check whether some content(s) (aka "blob(s)") is present in the SWH archive + based on its *sha1* checksum. - - a GET request with one or several hashes, separated by ',' - - a POST request with one or several hashes, passed as (multiple) values - for parameter 'q' + :param string sha1: hexadecimal representation of the *sha1* checksum value + for the content to check existence. Multiple values can be provided separated + by ','. - """ + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json array search_res: array holding the search result for each provided *sha1* + :>json object search_stats: some statistics regarding the number of *sha1* provided + and the percentage of those found in the SWH archive + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *sha1* has been provided + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`content/known/dc2830a9e72f23c1dfebef4413003221baa5fb62,0c3f19cb47ebfbe643fb19fa94c874d18fa62d12/` + """ # noqa response = {'search_res': None, 'search_stats': None} search_stats = {'nbfiles': 0, 'pct': 0} search_res = None queries = [] # GET: Many hash separated values request if q: hashes = q.split(',') for v in hashes: queries.append({'filename': None, 'sha1': v}) # POST: Many hash requests in post form submission elif request.method == 'POST': data = request.data # Remove potential inputs with no associated value for k, v in data.items(): if v is not None: if k == 'q' and len(v) > 0: queries.append({'filename': None, 'sha1': v}) elif v != '': queries.append({'filename': k, 'sha1': v}) if queries: lookup = service.lookup_multiple_hashes(queries) result = [] nb_queries = len(queries) for el in lookup: res_d = {'sha1': el['sha1'], 'found': el['found']} if 'filename' in el and el['filename']: res_d['filename'] = el['filename'] result.append(res_d) search_res = result nbfound = len([x for x in lookup if x['found']]) search_stats['nbfiles'] = nb_queries search_stats['pct'] = (nbfound / nb_queries) * 100 response['search_res'] = search_res response['search_stats'] = search_stats return response @api_route(r'/content/(?P.+)/', 'content') -@api_doc.route('/content/') -@api_doc.arg('q', - default='dc2830a9e72f23c1dfebef4413003221baa5fb62', - argtype=api_doc.argtypes.algo_and_hash, - argdoc=doc_arg_content_id) -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""known metadata for content identified by q""") +@api_doc('/content/') def api_content_metadata(request, q): - """Get information about a content (AKA "blob") object. - """ + .. http:get:: /api/1/content/[(hash_type):](hash)/ + + Get information about a content (aka a "blob") object. + In the SWH archive, a content object is identified based on checksum + values computed using various hashing algorithms. + + :param string hash_type: optional parameter specifying which hashing algorithm has been used + to compute the content checksum. It can be either *sha1*, *sha1_git*, *sha256* + or *blake2s256*. If that parameter is not provided, it is assumed that the + hashing algorithm used is *sha1*. + :param string hash: hexadecimal representation of the checksum value computed with + the specified hashing algorithm. + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json object checksums: object holding the computed checksum values for the requested content + :>json string data_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/raw/` + for downloading the content raw bytes + :>json string filetype_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/filetype/` + for getting information about the content MIME type + :>json string language_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/language/` + for getting information about the programming language used in the content + :>json number length: length of the content in bytes + :>json string license_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/license/` + for getting information about the license of the content + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *hash_type* or *hash* has been provided + :statuscode 404: requested content can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + curl -i :swh_web_api:`content/sha1_git:fe95a46679d128ff167b7c55df5d02356c5a1ae1/` + """ # noqa return api_lookup( service.lookup_content, q, notfound_msg='Content with {} not found.'.format(q), enrich_fn=functools.partial(utils.enrich_content, query_string=q)) diff --git a/swh/web/api/views/directory.py b/swh/web/api/views/directory.py index e7b90c60..5eeb3f97 100644 --- a/swh/web/api/views/directory.py +++ b/swh/web/api/views/directory.py @@ -1,59 +1,74 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from swh.web.common import service from swh.web.api import utils -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api.apiurls import api_route -from swh.web.api.views.utils import ( - api_lookup, doc_exc_id_not_found, - doc_exc_bad_id, -) +from swh.web.api.views.utils import api_lookup @api_route(r'/directory/(?P[0-9a-f]+)/', 'directory') @api_route(r'/directory/(?P[0-9a-f]+)/(?P.+)/', 'directory') -@api_doc.route('/directory/') -@api_doc.arg('sha1_git', - default='1bd0e65f7d2ff14ae994de17a1e7fe65111dcad8', - argtype=api_doc.argtypes.sha1_git, - argdoc='directory identifier') -@api_doc.arg('path', - default='codec/demux', - argtype=api_doc.argtypes.path, - argdoc='path relative to directory identified by sha1_git') -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""either a list of directory entries with their metadata, - or the metadata of a single directory entry""") +@api_doc('/directory/') def api_directory(request, sha1_git, path=None): - """Get information about directory or directory entry objects. + """ + .. http:get:: /api/1/directory/(sha1_git)/[(path)/] - Directories are identified by SHA1 checksums, compatible with Git directory - identifiers. See the `documentation - `_ - for details about how they are computed. + Get information about directory objects. + Directories are identified by *sha1* checksums, compatible with Git directory identifiers. + See :func:`swh.model.identifiers.directory_identifier` in our data model module for details + about how they are computed. - When given only a directory identifier, this endpoint returns information - about the directory itself, returning its content (usually a list of - directory entries). When given a directory identifier and a path, this - endpoint returns information about the directory entry pointed by the - relative path, starting path resolution from the given directory. + When given only a directory identifier, this endpoint returns information about the directory itself, + returning its content (usually a list of directory entries). When given a directory identifier and a + path, this endpoint returns information about the directory entry pointed by the relative path, + starting path resolution from the given directory. - """ + :param string sha1_git: hexadecimal representation of the directory *sha1_git* identifier + :param string path: optional parameter to get information about the directory entry + pointed by that relative path + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>jsonarr object checksums: object holding the computed checksum values for a directory entry + (only for file entries) + :>jsonarr string dir_id: *sha1_git* identifier of the requested directory + :>jsonarr number length: length of a directory entry in bytes (only for file entries) + for getting information about the content MIME type + :>jsonarr string name: the directory entry name + :>jsonarr number perms: permissions for the directory entry + :>jsonarr string target: *sha1_git* identifier of the directory entry + :>jsonarr string target_url: link to :http:get:`/api/1/content/[(hash_type):](hash)/` + or :http:get:`/api/1/directory/(sha1_git)/[(path)/]` depending on the directory entry type + :>jsonarr string type: the type of the directory entry, can be either *dir*, *file* or *rev* + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *hash_type* or *hash* has been provided + :statuscode 404: requested directory can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`directory/977fc4b98c0e85816348cebd3b12026407c368b6/` + """ # noqa if path: error_msg_path = ('Entry with path %s relative to directory ' 'with sha1_git %s not found.') % (path, sha1_git) return api_lookup( service.lookup_directory_with_path, sha1_git, path, notfound_msg=error_msg_path, enrich_fn=utils.enrich_directory) else: error_msg_nopath = 'Directory with sha1_git %s not found.' % sha1_git return api_lookup( service.lookup_directory, sha1_git, notfound_msg=error_msg_nopath, enrich_fn=utils.enrich_directory) diff --git a/swh/web/api/views/entity.py b/swh/web/api/views/entity.py index daafcab8..c459cb96 100644 --- a/swh/web/api/views/entity.py +++ b/swh/web/api/views/entity.py @@ -1,33 +1,22 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from swh.web.common import service from swh.web.api import utils -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api.apiurls import api_route -from swh.web.api.views.utils import ( - api_lookup, doc_exc_id_not_found, - doc_exc_bad_id -) +from swh.web.api.views.utils import api_lookup @api_route(r'/entity/(?P.+)/', 'entity') -@api_doc.route('/entity/', tags=['hidden']) -@api_doc.arg('uuid', - default='5f4d4c51-498a-4e28-88b3-b3e4e8396cba', - argtype=api_doc.argtypes.uuid, - argdoc="The entity's uuid identifier") -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='The metadata of the entity identified by uuid') +@api_doc('/entity/', tags=['hidden']) def api_entity_by_uuid(request, uuid): """Return content information if content is found. """ return api_lookup( service.lookup_entity_by_uuid, uuid, notfound_msg="Entity with uuid '%s' not found." % uuid, enrich_fn=utils.enrich_entity) diff --git a/swh/web/api/views/origin.py b/swh/web/api/views/origin.py index 80b7826d..db13c3c2 100644 --- a/swh/web/api/views/origin.py +++ b/swh/web/api/views/origin.py @@ -1,267 +1,337 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from distutils.util import strtobool from swh.web.common import service from swh.web.common.utils import ( reverse, get_origin_visits ) from swh.web.api import utils -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api.apiurls import api_route -from swh.web.api.views.utils import ( - api_lookup, doc_exc_id_not_found, doc_header_link, - doc_arg_last_elt, doc_arg_per_page -) +from swh.web.api.views.utils import api_lookup def _enrich_origin(origin): if 'id' in origin: o = origin.copy() o['origin_visits_url'] = \ reverse('origin-visits', kwargs={'origin_id': origin['id']}) return o return origin @api_route(r'/origin/(?P[0-9]+)/', 'origin') @api_route(r'/origin/(?P[a-z]+)/url/(?P.+)', 'origin') -@api_doc.route('/origin/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc='origin identifier (when looking up by ID)') -@api_doc.arg('origin_type', - default='git', - argtype=api_doc.argtypes.str, - argdoc='origin type (when looking up by type+URL)') -@api_doc.arg('origin_url', - default='https://github.com/hylang/hy', - argtype=api_doc.argtypes.path, - argdoc='origin URL (when looking up by type+URL)') -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""The metadata of the origin corresponding to the given - criteria""") +@api_doc('/origin/') def api_origin(request, origin_id=None, origin_type=None, origin_url=None): - """Get information about a software origin. + """ + .. http:get:: /api/1/origin/(origin_id)/ - Software origins might be looked up by origin type and canonical URL (e.g., - "git" + a "git clone" URL), or by their unique (but otherwise meaningless) - identifier. + Get information about a software origin. - """ + :param int origin_id: a SWH origin identifier + + :>json number id: the origin unique identifier + :>json string origin_visits_url: link to in order to get information about the SWH + visits for that origin + :>json string type: the type of software origin (*git*, *svn*, *hg*, *deb*, *ftp*, ...) + :>json string url: the origin canonical url + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 404: requested origin can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`origin/1/` + + .. http:get:: /api/1/origin/(origin_type)/url/(origin_url)/ + + Get information about a software origin. + + :param string origin_type: the origin type (*git*, *svn*, *hg*, *deb*, *ftp*, ...) + :param string origin_url: the origin url + + :>json number id: the origin unique identifier + :>json string origin_visits_url: link to in order to get information about the SWH + visits for that origin + :>json string type: the type of software origin (*git*, *svn*, *hg*, *deb*, *ftp*, ...) + :>json string url: the origin canonical url + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 404: requested origin can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`origin/git/url/https://github.com/python/cpython/` + """ # noqa ori_dict = { 'id': origin_id, 'type': origin_type, 'url': origin_url } ori_dict = {k: v for k, v in ori_dict.items() if ori_dict[k]} if 'id' in ori_dict: error_msg = 'Origin with id %s not found.' % ori_dict['id'] else: error_msg = 'Origin with type %s and URL %s not found' % ( ori_dict['type'], ori_dict['url']) return api_lookup( service.lookup_origin, ori_dict, notfound_msg=error_msg, enrich_fn=_enrich_origin) @api_route(r'/origin/search/(?P.+)/', 'origin-search') -@api_doc.route('/origin/search/') -@api_doc.arg('url_pattern', - default='python', - argtype=api_doc.argtypes.str, - argdoc='string pattern to search for in origin urls') -@api_doc.header('Link', doc=doc_header_link) -@api_doc.param('offset', - default=0, - argtype=api_doc.argtypes.int, - doc='number of found origins to skip before returning results') # noqa -@api_doc.param('limit', - default=70, - argtype=api_doc.argtypes.int, - doc='the maximum number of found origins to return') -@api_doc.param('regexp', - default='false', - argtype=api_doc.argtypes.str, - doc="""if that query parameter is set to 'true', consider provided - pattern as a regular expression and search origins whose - urls match it""") -@api_doc.returns(rettype=api_doc.rettypes.list, - retdoc="""The metadata of the origins whose urls match - the provided string pattern""") +@api_doc('/origin/search/') def api_origin_search(request, url_pattern): - """Search for origins whose urls contain a provided string pattern - or match a provided regular expression. - The search is performed in a case insensitive way. - """ + .. http:get:: /api/1/origin/search/(url_pattern)/ + + Search for software origins whose urls contain a provided string + pattern or match a provided regular expression. + The search is performed in a case insensitive way. + + :param string url_pattern: a string pattern or a regular expression + :query int offset: the number of found origins to skip before returning results + :query int limit: the maximum number of found origins to return + :query boolean regexp: if true, consider provided pattern as a regular expression + and search origins whose urls match it + + :>jsonarr number id: the origin unique identifier + :>jsonarr string origin_visits_url: link to in order to get information about the SWH + visits for that origin + :>jsonarr string type: the type of software origin (*git*, *svn*, *hg*, *deb*, *ftp*, ...) + :>jsonarr string url: the origin canonical url + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`origin/search/python/?limit=2` + """ # noqa result = {} offset = int(request.query_params.get('offset', '0')) limit = int(request.query_params.get('limit', '70')) regexp = request.query_params.get('regexp', 'false') results = api_lookup(service.search_origin, url_pattern, offset, limit, bool(strtobool(regexp)), enrich_fn=_enrich_origin) nb_results = len(results) if nb_results == limit: query_params = {} query_params['offset'] = offset + limit query_params['limit'] = limit query_params['regexp'] = regexp result['headers'] = { 'link-next': reverse('origin-search', kwargs={'url_pattern': url_pattern}, query_params=query_params) } result.update({ 'results': results }) return result @api_route(r'/origin/(?P[0-9]+)/visits/', 'origin-visits') -@api_doc.route('/origin/visits/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc='software origin identifier') -@api_doc.header('Link', doc=doc_header_link) -@api_doc.param('last_visit', default=None, - argtype=api_doc.argtypes.int, - doc=doc_arg_last_elt) -@api_doc.param('per_page', default=10, - argtype=api_doc.argtypes.int, - doc=doc_arg_per_page) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.list, - retdoc="""a list of dictionaries describing individual visits. - For each visit, its identifier, timestamp (as UNIX time), - outcome, snapshot id, and visit-specific URL for more - information are given. Visits are sorted in descending - order according to their timestamp.""") +@api_doc('/origin/visits/') def api_origin_visits(request, origin_id): - """Get information about all visits of a given software origin. - """ + .. http:get:: /api/1/origin/(origin_id)/visits/ + + Get information about all visits of a software origin. + Visits are returned sorted in descending order according + to their date. + + :param int origin_id: a SWH origin identifier + :query int per_page: specify the number of visits to list, for pagination purposes + :query int last_visit: visit to start listing from, for pagination purposes + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + :resheader Link: indicates that a subsequent result page is available and contains + the url pointing to it + + :>jsonarr string date: ISO representation of the visit date (in UTC) + :>jsonarr number id: the unique identifier of the origin + :>jsonarr string origin_visit_url: link to :http:get:`/api/1/origin/(origin_id)/visit/(visit_id)/` + in order to get information about the visit + :>jsonarr string snapshot: the snapshot identifier of the visit + :>jsonarr string snapshot_url: link to :http:get:`/api/1/snapshot/(snapshot_id)/` + in order to get information about the snapshot of the visit + :>jsonarr string status: status of the visit (either *full*, *partial* or *ongoing*) + :>jsonarr number visit: the unique identifier of the visit + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 404: requested origin can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`origin/1/visits/` + """ # noqa result = {} per_page = int(request.query_params.get('per_page', '10')) last_visit = request.query_params.get('last_visit') if last_visit: last_visit = int(last_visit) def _lookup_origin_visits( origin_id, last_visit=last_visit, per_page=per_page): all_visits = get_origin_visits({'id': origin_id}) all_visits.reverse() visits = [] if not last_visit: visits = all_visits[:per_page] else: for i, v in enumerate(all_visits): if v['visit'] == last_visit: visits = all_visits[i+1:i+1+per_page] break for v in visits: yield v def _enrich_origin_visit(origin_visit): ov = origin_visit.copy() ov['origin_visit_url'] = reverse('origin-visit', kwargs={'origin_id': origin_id, 'visit_id': ov['visit']}) snapshot = ov['snapshot'] if snapshot: ov['snapshot_url'] = reverse('snapshot', kwargs={'snapshot_id': snapshot}) else: ov['snapshot_url'] = None return ov results = api_lookup(_lookup_origin_visits, origin_id, notfound_msg='No origin {} found'.format(origin_id), enrich_fn=_enrich_origin_visit) if results: nb_results = len(results) if nb_results == per_page: new_last_visit = results[-1]['visit'] query_params = {} query_params['last_visit'] = new_last_visit if request.query_params.get('per_page'): query_params['per_page'] = per_page result['headers'] = { 'link-next': reverse('origin-visits', kwargs={'origin_id': origin_id}, query_params=query_params) } result.update({ 'results': results }) return result @api_route(r'/origin/(?P[0-9]+)/visit/(?P[0-9]+)/', 'origin-visit') -@api_doc.route('/origin/visit/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc='software origin identifier') -@api_doc.arg('visit_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc="""visit identifier, relative to the origin identified by - origin_id""") -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""dictionary containing both metadata for the entire - visit (e.g., timestamp as UNIX time, visit outcome, etc.) and - what was at the software origin during the visit (i.e., a - mapping from branches to other archive objects)""") +@api_doc('/origin/visit/') def api_origin_visit(request, origin_id, visit_id): - """Get information about a specific visit of a software origin. - """ + .. http:get:: /api/1/origin/(origin_id)/visit/(visit_id)/ + + Get information about a specific visit of a software origin. + + :param int origin_id: a SWH origin identifier + :param int visit_id: a visit identifier + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json string date: ISO representation of the visit date (in UTC) + :>json object occurrences: object containing all branches associated to the origin found + during the visit, for each of them the associated SWH target type and id are given + but also a link to get information about that target + :>json number origin: the origin unique identifier + :>json string origin_url: link to get information about the origin + :>jsonarr string snapshot: the snapshot identifier of the visit + :>jsonarr string snapshot_url: link to :http:get:`/api/1/snapshot/(snapshot_id)/` + in order to get information about the snapshot of the visit + :>json string status: status of the visit (either *full*, *partial* or *ongoing*) + :>json number visit: the unique identifier of the visit + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 404: requested origin or visit can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`origin/1500/visit/1/` + """ # noqa def _enrich_origin_visit(origin_visit): ov = origin_visit.copy() ov['origin_url'] = reverse('origin', kwargs={'origin_id': ov['origin']}) snapshot = ov['snapshot'] if snapshot: ov['snapshot_url'] = reverse('snapshot', kwargs={'snapshot_id': snapshot}) else: ov['snapshot_url'] = None # TODO: remove that piece of code once the snapshot migration # is totally effective in storage (no more occurrences) if 'occurrences' in ov: ov['occurrences'] = { k: utils.enrich_object(v) if v else None for k, v in ov['occurrences'].items() } return ov return api_lookup( service.lookup_origin_visit, origin_id, visit_id, notfound_msg=('No visit {} for origin {} found' .format(visit_id, origin_id)), enrich_fn=_enrich_origin_visit) diff --git a/swh/web/api/views/person.py b/swh/web/api/views/person.py index e3d74042..2e7ebe75 100644 --- a/swh/web/api/views/person.py +++ b/swh/web/api/views/person.py @@ -1,29 +1,44 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from swh.web.common import service -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api.apiurls import api_route -from swh.web.api.views.utils import ( - api_lookup, doc_exc_id_not_found, -) +from swh.web.api.views.utils import api_lookup @api_route(r'/person/(?P[0-9]+)/', 'person') -@api_doc.route('/person/') -@api_doc.arg('person_id', - default=42, - argtype=api_doc.argtypes.int, - argdoc='person identifier') -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='The metadata of the person identified by person_id') +@api_doc('/person/') def api_person(request, person_id): - """Get information about a person. - """ + .. http:get:: /api/1/person/(person_id)/ + + Get information about a person in the SWH archive. + + :param int person_id: a SWH person identifier + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json string email: the email of the person + :>json string fullname: the full name of the person: combination of its name and email + :>json number id: the unique identifier of the person + :>json string name: the name of the person + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 404: requested person can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`person/8275/` + """ # noqa return api_lookup( service.lookup_person, person_id, notfound_msg='Person with id {} not found.'.format(person_id)) diff --git a/swh/web/api/views/release.py b/swh/web/api/views/release.py index 3f4df5c9..80234dd9 100644 --- a/swh/web/api/views/release.py +++ b/swh/web/api/views/release.py @@ -1,38 +1,58 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from swh.web.common import service from swh.web.api import utils -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api.apiurls import api_route -from swh.web.api.views.utils import ( - api_lookup, doc_exc_id_not_found, doc_exc_bad_id -) +from swh.web.api.views.utils import api_lookup @api_route(r'/release/(?P[0-9a-f]+)/', 'release') -@api_doc.route('/release/') -@api_doc.arg('sha1_git', - default='7045404f3d1c54e6473c71bbb716529fbad4be24', - argtype=api_doc.argtypes.sha1_git, - argdoc='release identifier') -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='The metadata of the release identified by sha1_git') +@api_doc('/release/') def api_release(request, sha1_git): - """Get information about a release. + """ + .. http:get:: /api/1/release/(sha1_git)/ - Releases are identified by SHA1 checksums, compatible with Git tag - identifiers. See the `documentation - `_ - for details about how they are computed. + Get information about a release in the SWH archive. + Releases are identified by *sha1* checksums, compatible with Git tag identifiers. + See :func:`swh.model.identifiers.release_identifier` in our data model module for details + about how they are computed. - """ + :param string sha1_git: hexadecimal representation of the release *sha1_git* identifier + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json object author: information about the author of the release + :>json string author_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the author of the release + :>json string date: ISO representation of the release date (in UTC) + :>json string id: the release unique identifier + :>json string message: the message associated to the release + :>json string name: the name of the release + :>json string target: the target identifier of the release + :>json string target_type: the type of the target, can be either *release*, + *revision*, *content*, *directory* + :>json string target_url: a link to the adequate api url based on the target type + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *sha1_git* value has been provided + :statuscode 404: requested release can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`release/208f61cc7a5dbc9879ae6e5c2f95891e270f09ef/` + """ # noqa error_msg = 'Release with sha1_git %s not found.' % sha1_git return api_lookup( service.lookup_release, sha1_git, notfound_msg=error_msg, enrich_fn=utils.enrich_release) diff --git a/swh/web/api/views/revision.py b/swh/web/api/views/revision.py index 62d6f1a9..d178661b 100644 --- a/swh/web/api/views/revision.py +++ b/swh/web/api/views/revision.py @@ -1,422 +1,504 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from django.http import HttpResponse from swh.web.common import service from swh.web.common.utils import reverse from swh.web.common.utils import parse_timestamp from swh.web.api import utils -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api.apiurls import api_route -from swh.web.api.views.utils import ( - api_lookup, doc_exc_id_not_found, doc_header_link, - doc_arg_per_page, doc_exc_bad_id, - doc_ret_revision_log, doc_ret_revision_meta -) +from swh.web.api.views.utils import api_lookup def _revision_directory_by(revision, path, request_path, limit=100, with_data=False): - """Compute the revision matching criterion's directory or content data. + """ + Compute the revision matching criterion's directory or content data. Args: revision: dictionary of criterions representing a revision to lookup path: directory's path to lookup request_path: request path which holds the original context to limit: optional query parameter to limit the revisions log (default to 100). For now, note that this limit could impede the transitivity conclusion about sha1_git not being an ancestor of with_data: indicate to retrieve the content's raw data if path resolves to a content. """ def enrich_directory_local(dir, context_url=request_path): return utils.enrich_directory(dir, context_url) rev_id, result = service.lookup_directory_through_revision( revision, path, limit=limit, with_data=with_data) content = result['content'] if result['type'] == 'dir': # dir_entries result['content'] = list(map(enrich_directory_local, content)) else: # content result['content'] = utils.enrich_content(content) return result @api_route(r'/revision/origin/(?P[0-9]+)' r'/branch/(?P.+)/log/', 'revision-origin-log') @api_route(r'/revision/origin/(?P[0-9]+)/log/', 'revision-origin-log') @api_route(r'/revision/origin/(?P[0-9]+)' r'/ts/(?P.+)/log/', 'revision-origin-log') @api_route(r'/revision/origin/(?P[0-9]+)' r'/branch/(?P.+)' r'/ts/(?P.+)/log/', 'revision-origin-log') -@api_doc.route('/revision/origin/log/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc="The revision's SWH origin identifier") -@api_doc.arg('branch_name', - default='refs/heads/master', - argtype=api_doc.argtypes.path, - argdoc="""(Optional) The revision's branch name within the origin specified. -Defaults to 'refs/heads/master'.""") -@api_doc.arg('ts', - default='2000-01-17T11:23:54+00:00', - argtype=api_doc.argtypes.ts, - argdoc="""(Optional) A time or timestamp string to parse""") -@api_doc.header('Link', doc=doc_header_link) -@api_doc.param('per_page', default=10, - argtype=api_doc.argtypes.int, - doc=doc_arg_per_page) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=doc_ret_revision_log) +@api_doc('/revision/origin/log/') def api_revision_log_by(request, origin_id, branch_name='refs/heads/master', ts=None): - """Show the commit log for a revision, searching for it based on software - origin, branch name, and/or visit timestamp. - - This endpoint behaves like ``/log``, but operates on the revision that - has been found at a given software origin, close to a given point in time, - pointed by a given branch. """ + .. http:get:: /api/1/revision/origin/(origin_id)[/branch/(branch_name)][/ts/(timestamp)]/log + + Show the commit log for a revision, searching for it based on software origin, + branch name, and/or visit timestamp. + + This endpoint behaves like :http:get:`/api/1/revision/(sha1_git)[/prev/(prev_sha1s)]/log/`, + but operates on the revision that has been found at a given software origin, + close to a given point in time, pointed by a given branch. + + :param int origin_id: a SWH origin identifier + :param string branch_name: optional parameter specifying a fully-qualified branch name + associated to the software origin, e.g., "refs/heads/master". Defaults to the master branch. + :param string timestamp: optional parameter specifying a timestamp close to which the revision + pointed by the given branch should be looked up. The timestamp can be expressed either + as an ISO date or as a Unix one (in UTC). Defaults to now. + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>jsonarr object author: information about the author of the revision + :>jsonarr string author_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the author of the revision + :>jsonarr object committer: information about the committer of the revision + :>jsonarr string committer_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the committer of the revision + :>jsonarr string committer_date: ISO representation of the commit date (in UTC) + :>jsonarr string date: ISO representation of the revision date (in UTC) + :>jsonarr string directory: the unique identifier that revision points to + :>jsonarr string directory_url: link to :http:get:`/api/1/directory/(sha1_git)/[(path)/]` + to get information about the directory associated to the revision + :>jsonarr string id: the revision unique identifier + :>jsonarr boolean merge: whether or not the revision corresponds to a merge commit + :>jsonarr string message: the message associated to the revision + :>jsonarr array parents: the parents of the revision, i.e. the previous revisions + that head directly to it, each entry of that array contains an unique parent + revision identifier but also a link to :http:get:`/api/1/revision/(sha1_git)/` + to get more informations about it + :>jsonarr string type: the type of the revision + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 404: no revision matching the given criteria could be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`revision/origin/723566/ts/2016-01-17T00:00:00+00:00/log/` + """ # noqa result = {} per_page = int(request.query_params.get('per_page', '10')) if ts: ts = parse_timestamp(ts) def lookup_revision_log_by_with_limit(o_id, br, ts, limit=per_page+1): return service.lookup_revision_log_by(o_id, br, ts, limit) error_msg = 'No revision matching origin %s ' % origin_id error_msg += ', branch name %s' % branch_name error_msg += (' and time stamp %s.' % ts) if ts else '.' rev_get = api_lookup( lookup_revision_log_by_with_limit, origin_id, branch_name, ts, notfound_msg=error_msg, enrich_fn=utils.enrich_revision) nb_rev = len(rev_get) if nb_rev == per_page+1: revisions = rev_get[:-1] last_sha1_git = rev_get[-1]['id'] params = {k: v for k, v in {'origin_id': origin_id, 'branch_name': branch_name, 'ts': ts, }.items() if v is not None} query_params = {} query_params['sha1_git'] = last_sha1_git if request.query_params.get('per_page'): query_params['per_page'] = per_page result['headers'] = { 'link-next': reverse('revision-origin-log', kwargs=params, query_params=query_params) } else: revisions = rev_get result.update({'results': revisions}) return result @api_route(r'/revision/origin/(?P[0-9]+)/directory/', 'revision-directory') @api_route(r'/revision/origin/(?P[0-9]+)/directory/(?P.+)/', 'revision-directory') @api_route(r'/revision/origin/(?P[0-9]+)' r'/branch/(?P.+)/directory/', 'revision-directory') @api_route(r'/revision/origin/(?P[0-9]+)' r'/branch/(?P.+)/ts/(?P.+)/directory/', 'revision-directory') @api_route(r'/revision/origin/(?P[0-9]+)' r'/branch/(?P.+)/directory/(?P.+)/', 'revision-directory') @api_route(r'/revision/origin/(?P[0-9]+)' r'/branch/(?P.+)/ts/(?P.+)' r'/directory/(?P.+)/', 'revision-directory') -@api_doc.route('/revision/origin/directory/', tags=['hidden']) -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc="The revision's origin's SWH identifier") -@api_doc.arg('branch_name', - default='refs/heads/master', - argtype=api_doc.argtypes.path, - argdoc="""The optional branch for the given origin (default - to master""") -@api_doc.arg('ts', - default='2000-01-17T11:23:54+00:00', - argtype=api_doc.argtypes.ts, - argdoc="""Optional timestamp (default to the nearest time - crawl of timestamp)""") -@api_doc.arg('path', - default='Dockerfile', - argtype=api_doc.argtypes.path, - argdoc='The path to the directory or file to display') -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""The metadata of the revision corresponding to the - given criteria""") +@api_doc('/revision/origin/directory/', tags=['hidden']) def api_directory_through_revision_origin(request, origin_id, branch_name="refs/heads/master", ts=None, path=None, with_data=False): - """Display directory or content information through a revision identified + """ + Display directory or content information through a revision identified by origin/branch/timestamp. """ if ts: ts = parse_timestamp(ts) return _revision_directory_by({'origin_id': origin_id, 'branch_name': branch_name, 'ts': ts }, path, request.path, with_data=with_data) @api_route(r'/revision/origin/(?P[0-9]+)/', 'revision-origin') @api_route(r'/revision/origin/(?P[0-9]+)' r'/branch/(?P.+)/', 'revision-origin') @api_route(r'/revision/origin/(?P[0-9]+)' r'/branch/(?P.+)/ts/(?P.+)/', 'revision-origin') @api_route(r'/revision/origin/(?P[0-9]+)/ts/(?P.+)/', 'revision-origin') -@api_doc.route('/revision/origin/') -@api_doc.arg('origin_id', - default=1, - argtype=api_doc.argtypes.int, - argdoc='software origin identifier') -@api_doc.arg('branch_name', - default='refs/heads/master', - argtype=api_doc.argtypes.path, - argdoc="""(optional) fully-qualified branch name, e.g., - "refs/heads/master". Defaults to the master branch.""") -@api_doc.arg('ts', - default=None, - argtype=api_doc.argtypes.ts, - argdoc="""(optional) timestamp close to which the revision pointed by - the given branch should be looked up. Defaults to now.""") -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=doc_ret_revision_meta) +@api_doc('/revision/origin/') def api_revision_with_origin(request, origin_id, branch_name="refs/heads/master", ts=None): - """Get information about a revision, searching for it based on software - origin, branch name, and/or visit timestamp. - - This endpoint behaves like ``/revision``, but operates on the revision that - has been found at a given software origin, close to a given point in time, - pointed by a given branch. - """ + .. http:get:: /api/1/revision/origin/(origin_id)/[branch/(branch_name)/][ts/(timestamp)/] + + Get information about a revision, searching for it based on software origin, + branch name, and/or visit timestamp. + + This endpoint behaves like :http:get:`/api/1/revision/(sha1_git)/`, + but operates on the revision that has been found at a given software origin, + close to a given point in time, pointed by a given branch. + + :param int origin_id: a SWH origin identifier + :param string branch_name: optional parameter specifying a fully-qualified branch name + associated to the software origin, e.g., "refs/heads/master". Defaults to the master branch. + :param string timestamp: optional parameter specifying a timestamp close to which the revision + pointed by the given branch should be looked up. The timestamp can be expressed either + as an ISO date or as a Unix one (in UTC). Defaults to now. + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json object author: information about the author of the revision + :>json string author_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the author of the revision + :>json object committer: information about the committer of the revision + :>json string committer_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the committer of the revision + :>json string committer_date: ISO representation of the commit date (in UTC) + :>json string date: ISO representation of the revision date (in UTC) + :>json string directory: the unique identifier that revision points to + :>json string directory_url: link to :http:get:`/api/1/directory/(sha1_git)/[(path)/]` + to get information about the directory associated to the revision + :>json string id: the revision unique identifier + :>json boolean merge: whether or not the revision corresponds to a merge commit + :>json string message: the message associated to the revision + :>json array parents: the parents of the revision, i.e. the previous revisions + that head directly to it, each entry of that array contains an unique parent + revision identifier but also a link to :http:get:`/api/1/revision/(sha1_git)/` + to get more informations about it + :>json string type: the type of the revision + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 404: no revision matching the given criteria could be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`revision/origin/13706355/branch/refs/heads/2.7/` + """ # noqa ts = parse_timestamp(ts) return api_lookup( service.lookup_revision_by, origin_id, branch_name, ts, notfound_msg=('Revision with (origin_id: {}, branch_name: {}' ', ts: {}) not found.'.format(origin_id, branch_name, ts)), enrich_fn=utils.enrich_revision) @api_route(r'/revision/(?P[0-9a-f]+)/prev/(?P[0-9a-f/]+)/', 'revision-context') -@api_doc.route('/revision/prev/', tags=['hidden']) -@api_doc.arg('sha1_git', - default='ec72c666fb345ea5f21359b7bc063710ce558e39', - argtype=api_doc.argtypes.sha1_git, - argdoc="The revision's sha1_git identifier") -@api_doc.arg('context', - default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a', - argtype=api_doc.argtypes.path, - argdoc='The navigation breadcrumbs -- use at your own risk') -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='The metadata of the revision identified by sha1_git') +@api_doc('/revision/prev/', tags=['hidden']) def api_revision_with_context(request, sha1_git, context): - """Return information about revision with id sha1_git. + """ + Return information about revision with id sha1_git. """ def _enrich_revision(revision, context=context): return utils.enrich_revision(revision, context) return api_lookup( service.lookup_revision, sha1_git, notfound_msg='Revision with sha1_git %s not found.' % sha1_git, enrich_fn=_enrich_revision) @api_route(r'/revision/(?P[0-9a-f]+)/', 'revision') -@api_doc.route('/revision/') -@api_doc.arg('sha1_git', - default='aafb16d69fd30ff58afdd69036a26047f3aebdc6', - argtype=api_doc.argtypes.sha1_git, - argdoc="revision identifier") -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=doc_ret_revision_meta) +@api_doc('/revision/') def api_revision(request, sha1_git): - """Get information about a revision. - - Revisions are identified by SHA1 checksums, compatible with Git commit - identifiers. See the `documentation - `_ - for details about how they are computed. - """ + .. http:get:: /api/1/revision/(sha1_git)/ + + Get information about a revision in the SWH archive. + Revisions are identified by *sha1* checksums, compatible with Git commit identifiers. + See :func:`swh.model.identifiers.revision_identifier` in our data model module for details + about how they are computed. + + :param string sha1_git: hexadecimal representation of the revision *sha1_git* identifier + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json object author: information about the author of the revision + :>json string author_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the author of the revision + :>json object committer: information about the committer of the revision + :>json string committer_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the committer of the revision + :>json string committer_date: ISO representation of the commit date (in UTC) + :>json string date: ISO representation of the revision date (in UTC) + :>json string directory: the unique identifier that revision points to + :>json string directory_url: link to :http:get:`/api/1/directory/(sha1_git)/[(path)/]` + to get information about the directory associated to the revision + :>json string id: the revision unique identifier + :>json boolean merge: whether or not the revision corresponds to a merge commit + :>json string message: the message associated to the revision + :>json array parents: the parents of the revision, i.e. the previous revisions + that head directly to it, each entry of that array contains an unique parent + revision identifier but also a link to :http:get:`/api/1/revision/(sha1_git)/` + to get more informations about it + :>json string type: the type of the revision + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *sha1_git* value has been provided + :statuscode 404: requested revision can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/` + """ # noqa return api_lookup( service.lookup_revision, sha1_git, notfound_msg='Revision with sha1_git {} not found.'.format(sha1_git), enrich_fn=utils.enrich_revision) @api_route(r'/revision/(?P[0-9a-f]+)/raw/', 'revision-raw-message') -@api_doc.route('/revision/raw/', tags=['hidden'], handle_response=True) -@api_doc.arg('sha1_git', - default='ec72c666fb345ea5f21359b7bc063710ce558e39', - argtype=api_doc.argtypes.sha1_git, - argdoc="The queried revision's sha1_git identifier") -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.octet_stream, - retdoc="""The message of the revision identified by sha1_git - as a downloadable octet stream""") +@api_doc('/revision/raw/', tags=['hidden'], handle_response=True) def api_revision_raw_message(request, sha1_git): """Return the raw data of the message of revision identified by sha1_git """ raw = service.lookup_revision_message(sha1_git) response = HttpResponse(raw['message'], content_type='application/octet-stream') response['Content-disposition'] = \ 'attachment;filename=rev_%s_raw' % sha1_git return response @api_route(r'/revision/(?P[0-9a-f]+)/directory/', 'revision-directory') @api_route(r'/revision/(?P[0-9a-f]+)/directory/(?P.+)/', 'revision-directory') -@api_doc.route('/revision/directory/') -@api_doc.arg('sha1_git', - default='ec72c666fb345ea5f21359b7bc063710ce558e39', - argtype=api_doc.argtypes.sha1_git, - argdoc='revision identifier') -@api_doc.arg('dir_path', - default='Documentation/BUG-HUNTING', - argtype=api_doc.argtypes.path, - argdoc="""path relative to the root directory of revision identifier by - sha1_git""") -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""either a list of directory entries with their metadata, - or the metadata of a single directory entry""") +@api_doc('/revision/directory/') def api_revision_directory(request, sha1_git, dir_path=None, with_data=False): - """Get information about directory (entry) objects associated to revisions. + """ + .. http:get:: /api/1/revision/(sha1_git)/directory/[(path)/] - Each revision is associated to a single "root" directory. This endpoint - behaves like ``/directory/``, but operates on the root directory associated - to a given revision. + Get information about directory (entry) objects associated to revisions. + Each revision is associated to a single "root" directory. + This endpoint behaves like :http:get:`/api/1/directory/(sha1_git)/[(path)/]`, + but operates on the root directory associated to a given revision. - """ + :param string sha1_git: hexadecimal representation of the revision *sha1_git* identifier + :param string path: optional parameter to get information about the directory entry + pointed by that relative path + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json array content: directory entries as returned by :http:get:`/api/1/directory/(sha1_git)/[(path)/]` + :>json string path: path of directory from the revision root one + :>json string revision: the unique revision identifier + :>json string type: the type of the directory + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *sha1_git* value has been provided + :statuscode 404: requested revision can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`revision/f1b94134a4b879bc55c3dacdb496690c8ebdc03f/directory/` + """ # noqa return _revision_directory_by({'sha1_git': sha1_git}, dir_path, request.path, with_data=with_data) @api_route(r'/revision/(?P[0-9a-f]+)/log/', 'revision-log') @api_route(r'/revision/(?P[0-9a-f]+)' r'/prev/(?P[0-9a-f/]+)/log/', 'revision-log') -@api_doc.route('/revision/log/') -@api_doc.arg('sha1_git', - default='37fc9e08d0c4b71807a4f1ecb06112e78d91c283', - argtype=api_doc.argtypes.sha1_git, - argdoc='revision identifier') -@api_doc.arg('prev_sha1s', - default='6adc4a22f20bbf3bbc754f1ec8c82be5dfb5c71a', - argtype=api_doc.argtypes.path, - argdoc="""(Optional) Navigation breadcrumbs (descendant revisions -previously visited). If multiple values, use / as delimiter. """) -@api_doc.header('Link', doc=doc_header_link) -@api_doc.param('per_page', default=10, - argtype=api_doc.argtypes.int, - doc=doc_arg_per_page) -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, retdoc=doc_ret_revision_log) +@api_doc('/revision/log/') def api_revision_log(request, sha1_git, prev_sha1s=None): - """Get a list of all revisions heading to a given one, i.e., show the - commit log. - """ + .. http:get:: /api/1/revision/(sha1_git)[/prev/(prev_sha1s)]/log/ + + Get a list of all SWH revisions heading to a given one, in other words show the commit log. + + :param string sha1_git: hexadecimal representation of the revision *sha1_git* identifier + :param string prev_sha1s: optional parameter representing the navigation breadcrumbs + (descendant revisions previously visited). If multiple values, use / as delimiter. + If provided, revisions information will be added at the beginning of the returned list. + :query int per_page: number of elements in the returned list, for pagination purpose + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + :resheader Link: indicates that a subsequent result page is available and contains + the url pointing to it + + :>jsonarr object author: information about the author of the revision + :>jsonarr string author_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the author of the revision + :>jsonarr object committer: information about the committer of the revision + :>jsonarr string committer_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the committer of the revision + :>jsonarr string committer_date: ISO representation of the commit date (in UTC) + :>jsonarr string date: ISO representation of the revision date (in UTC) + :>jsonarr string directory: the unique identifier that revision points to + :>jsonarr string directory_url: link to :http:get:`/api/1/directory/(sha1_git)/[(path)/]` + to get information about the directory associated to the revision + :>jsonarr string id: the revision unique identifier + :>jsonarr boolean merge: whether or not the revision corresponds to a merge commit + :>jsonarr string message: the message associated to the revision + :>jsonarr array parents: the parents of the revision, i.e. the previous revisions + that head directly to it, each entry of that array contains an unique parent + revision identifier but also a link to :http:get:`/api/1/revision/(sha1_git)/` + to get more informations about it + :>jsonarr string type: the type of the revision + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *sha1_git* value has been provided + :statuscode 404: requested revision can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`revision/e1a315fa3fa734e2a6154ed7b5b9ae0eb8987aad/log/` + """ # noqa result = {} per_page = int(request.query_params.get('per_page', '10')) def lookup_revision_log_with_limit(s, limit=per_page+1): return service.lookup_revision_log(s, limit) error_msg = 'Revision with sha1_git %s not found.' % sha1_git rev_get = api_lookup(lookup_revision_log_with_limit, sha1_git, notfound_msg=error_msg, enrich_fn=utils.enrich_revision) nb_rev = len(rev_get) if nb_rev == per_page+1: rev_backward = rev_get[:-1] new_last_sha1 = rev_get[-1]['id'] query_params = {} if request.query_params.get('per_page'): query_params['per_page'] = per_page result['headers'] = { 'link-next': reverse('revision-log', kwargs={'sha1_git': new_last_sha1}, query_params=query_params) } else: rev_backward = rev_get if not prev_sha1s: # no nav breadcrumbs, so we're done revisions = rev_backward else: rev_forward_ids = prev_sha1s.split('/') rev_forward = api_lookup( service.lookup_revision_multiple, rev_forward_ids, notfound_msg=error_msg, enrich_fn=utils.enrich_revision) revisions = rev_forward + rev_backward result.update({ 'results': revisions }) return result diff --git a/swh/web/api/views/snapshot.py b/swh/web/api/views/snapshot.py index 0bcc071f..5799d499 100644 --- a/swh/web/api/views/snapshot.py +++ b/swh/web/api/views/snapshot.py @@ -1,55 +1,69 @@ # Copyright (C) 2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from swh.web.common import service -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api import utils from swh.web.api.apiurls import api_route -from swh.web.api.views.utils import ( - api_lookup, doc_exc_id_not_found, doc_exc_bad_id -) +from swh.web.api.views.utils import api_lookup @api_route(r'/snapshot/(?P[0-9a-f]+)/', 'snapshot') -@api_doc.route('/snapshot/') -@api_doc.arg('snapshot_id', - default='584b2fe3ce6218a96892e73bd76c2966bbc2a797', - argtype=api_doc.argtypes.sha1, - argdoc='snapshot identifier') -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='dictionnary referencing the different' - ' named branches the snapshot contains') +@api_doc('/snapshot/') def api_snapshot(request, snapshot_id): - """Get information about a snapshot. + """ + .. http:get:: /api/1/snapshot/(snapshot_id)/ - A snapshot is a set of named branches, which are pointers to objects at any - level of the Software Heritage DAG. It represents a full picture of an - origin at a given time. + Get information about a snapshot in the SWH archive. - As well as pointing to other objects in the Software Heritage DAG, branches - can also be aliases, in which case their target is the name of another - branch in the same snapshot, or dangling, in which case the target is - unknown. + A snapshot is a set of named branches, which are pointers to objects at any + level of the Software Heritage DAG. It represents a full picture of an + origin at a given time. - A snapshot identifier is a salted sha1. See the `documentation - `_ - for details about how they are computed. - """ + As well as pointing to other objects in the Software Heritage DAG, branches + can also be aliases, in which case their target is the name of another + branch in the same snapshot, or dangling, in which case the target is + unknown. + + A snapshot identifier is a salted sha1. See :func:`swh.model.identifiers.snapshot_identifier` + in our data model module for details about how they are computed. + + :param sha1 snapshot_id: a SWH snapshot identifier + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json object branches: object containing all branches associated to the snapshot, + for each of them the associated SWH target type and id are given but also + a link to get information about that target + :>json string id: the unique identifier of the snapshot + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid snapshot identifier has been provided + :statuscode 404: requested snapshot can not be found in the SWH archive + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`snapshot/6a3a2cf0b2b90ce7ae1cf0a221ed68035b686f5a/` + """ # noqa def _enrich_snapshot(snapshot): s = snapshot.copy() if 'branches' in s: s['branches'] = { k: utils.enrich_object(v) if v else None for k, v in s['branches'].items() } return s return api_lookup( service.lookup_snapshot, snapshot_id, notfound_msg='Snapshot with id {} not found.'.format(snapshot_id), enrich_fn=_enrich_snapshot) diff --git a/swh/web/api/views/stat.py b/swh/web/api/views/stat.py index 5af0d47d..0c6de479 100644 --- a/swh/web/api/views/stat.py +++ b/swh/web/api/views/stat.py @@ -1,20 +1,55 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from swh.web.common import service -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api.apiurls import api_route @api_route(r'/stat/counters/', 'stat-counters') -@api_doc.route('/stat/counters/', noargs=True) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc="""dictionary mapping object types to the amount of - corresponding objects currently available in the archive""") +@api_doc('/stat/counters/', noargs=True) def api_stats(request): - """Get statistics about the content of the archive. - """ + .. http:get:: /api/1/stat/counters/ + + Get statistics about the content of the archive. + + :>json number content: current number of content objects (aka files) in the SWH archive + :>json number directory: current number of directory objects in the SWH archive + :>json number directory_entry_dir: current number of SWH directory entries + pointing to others SWH directories in the SWH archive + :>json number directory_entry_file: current number of SWH directory entries + pointing to SWH content objects in the SWH archive + :>json number directory_entry_rev: current number of SWH directory entries + pointing to SWH revision objects (e.g. git submodules) in the SWH archive + :>json number entity: current number of SWH entities (a SWH entity is either + a *group_of_entities*, a *group_of_persons*, a *project*, a *person*, an *organization*, + or a *hosting* service) in the SWH archive + :>json number occurrence: current number of SWH occurrences (an occurrence may be assimilated + to a branch found during a SWH crawl of a repository) in the SWH archive + :>json number origin: current number of SWH origins (an origin is a "place" where code + source can be found, e.g. a git repository, a tarball, ...) in the SWH archive + :>json number person: current number of SWH persons (code source authors or commiters) + in the SWH archive + :>json number release: current number of SWH releases objects in the SWH archive + :>json number revision: current number of SWH revision objects (aka commits) in the SWH archive + :>json number skipped_content: current number of content objects (aka files) which where + not inserted in the SWH archive + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + + **Example:** + + .. parsed-literal:: + + :swh_web_api:`stat/counters/` + """ # noqa return service.stat_counters() diff --git a/swh/web/api/views/utils.py b/swh/web/api/views/utils.py index 651d2c52..47a97d0c 100644 --- a/swh/web/api/views/utils.py +++ b/swh/web/api/views/utils.py @@ -1,93 +1,73 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from rest_framework.response import Response from rest_framework.decorators import api_view from types import GeneratorType from swh.web.common.exc import NotFoundExc from swh.web.api.apiurls import APIUrls, api_route -# canned doc string snippets that are used in several doc strings -doc_arg_content_id = """A "[hash_type:]hash" content identifier, where - hash_type is one of "sha1" (the default), "sha1_git", "sha256", and hash is - a checksum obtained with the hash_type hashing algorithm.""" -doc_arg_last_elt = 'element to start listing from, for pagination purposes' -doc_arg_per_page = 'number of elements to list, for pagination purposes' - -doc_exc_bad_id = 'syntax error in the given identifier(s)' -doc_exc_id_not_found = 'no object matching the given criteria could be found' - -doc_ret_revision_meta = 'metadata of the revision identified by sha1_git' -doc_ret_revision_log = """list of dictionaries representing the metadata of - each revision found in the commit log heading to revision sha1_git. - For each commit at least the following information are returned: - author/committer, authoring/commit timestamps, revision id, commit message, - parent (i.e., immediately preceding) commits, "root" directory id.""" - -doc_header_link = """indicates that a subsequent result page is available, - pointing to it""" - def api_lookup(lookup_fn, *args, notfound_msg='Object not found', enrich_fn=None): """ Capture a redundant behavior of: - looking up the backend with a criteria (be it an identifier or checksum) passed to the function lookup_fn - if nothing is found, raise an NotFoundExc exception with error message notfound_msg. - Otherwise if something is returned: - either as list, map or generator, map the enrich_fn function to it and return the resulting data structure as list. - either as dict and pass to enrich_fn and return the dict enriched. Args: - lookup_fn: function expects one criteria and optional supplementary \*args. - notfound_msg: if nothing matching the criteria is found, raise NotFoundExc with this error message. - enrich_fn: Function to use to enrich the result returned by lookup_fn. Default to the identity function if not provided. - \*args: supplementary arguments to pass to lookup_fn. Raises: NotFoundExp or whatever `lookup_fn` raises. """ if enrich_fn is None: enrich_fn = (lambda x: x) res = lookup_fn(*args) if not res: raise NotFoundExc(notfound_msg) if isinstance(res, (map, list, GeneratorType)): return [enrich_fn(x) for x in res] return enrich_fn(res) @api_view(['GET', 'HEAD']) def api_home(request): return Response({}, template_name='api.html') APIUrls.add_url_pattern(r'^$', api_home, view_name='api-homepage') @api_route(r'/', 'endpoints') def api_endpoints(request): """Display the list of opened api endpoints. """ routes = APIUrls.get_app_endpoints().copy() for route, doc in routes.items(): doc['doc_intro'] = doc['docstring'].split('\n\n')[0] # Return a list of routes with consistent ordering env = { 'doc_routes': sorted(routes.items()) } return Response(env, template_name="api-endpoints.html") diff --git a/swh/web/api/views/vault.py b/swh/web/api/views/vault.py index 43927e1b..544cbebb 100644 --- a/swh/web/api/views/vault.py +++ b/swh/web/api/views/vault.py @@ -1,150 +1,203 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from django.http import HttpResponse from django.views.decorators.cache import never_cache from swh.model import hashutil from swh.web.common import service, query from swh.web.common.utils import reverse -from swh.web.api import apidoc as api_doc +from swh.web.api.apidoc import api_doc from swh.web.api.apiurls import api_route -from swh.web.api.views.utils import ( - api_lookup, doc_exc_id_not_found, doc_exc_bad_id, -) +from swh.web.api.views.utils import api_lookup # XXX: a bit spaghetti. Would be better with class-based views. def _dispatch_cook_progress(request, obj_type, obj_id): hex_id = hashutil.hash_to_hex(obj_id) object_name = obj_type.split('_')[0].title() if request.method == 'GET': return api_lookup( service.vault_progress, obj_type, obj_id, notfound_msg=("{} '{}' was never requested." .format(object_name, hex_id))) elif request.method == 'POST': email = request.POST.get('email', request.GET.get('email', None)) return api_lookup( service.vault_cook, obj_type, obj_id, email, notfound_msg=("{} '{}' not found." .format(object_name, hex_id))) -@api_route('/vault/directory/(?P[a-fA-F0-9]+)/', +@api_route(r'/vault/directory/(?P[a-fA-F0-9]+)/', 'vault-cook-directory', methods=['GET', 'POST'], throttle_scope='swh_vault_cooking') @never_cache -@api_doc.route('/vault/directory/', tags=['hidden']) -@api_doc.arg('dir_id', - default='d4a96ba891017d0d26c15e509b4e6515e40d75ee', - argtype=api_doc.argtypes.sha1_git, - argdoc="The directory's sha1 identifier") -@api_doc.param('email', default=None, - argtype=api_doc.argtypes.int, - doc="e-mail to notify when the bundle is ready") -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc=('dictionary mapping containing the status of ' - 'the cooking')) +@api_doc('/vault/directory/', tags=['hidden']) def api_vault_cook_directory(request, dir_id): - """Requests an archive of the directoy identified by dir_id. + """ + .. http:get:: /api/1/vault/directory/(dir_id)/ + .. http:post:: /api/1/vault/directory/(dir_id)/ - To import the directory in the current directory, use:: + Request the cooking of an archive for a directory or check + its cooking status. - $ tar xvf path/to/directory.tar.gz - """ + That endpoint enables to create a vault cooking task for a directory + through a POST request or check the status of a previously created one + through a GET request. + + To import the directory in the current directory, use:: + + $ tar xvf path/to/directory.tar.gz + + :param string dir_id: the directory's sha1 identifier + + :query string email: e-mail to notify when the archive is ready + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json string fetch_url: the url from which to download the archive once it has been cooked + (see :http:get:`/api/1/vault/directory/(dir_id)/raw/`) + :>json string obj_type: the type of object to cook (directory or revision) + :>json string progress_message: message describing the cooking task progress + :>json number id: the cooking task id + :>json string status: the cooking task status (new/pending/done/failed) + :>json string obj_id: the identifier of the object to cook + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`post`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid directory identifier has been provided + :statuscode 404: requested directory can not be found in the SWH archive + """ # noqa _, obj_id = query.parse_hash_with_algorithms_or_throws( dir_id, ['sha1'], 'Only sha1_git is supported.') res = _dispatch_cook_progress(request, 'directory', obj_id) res['fetch_url'] = reverse('vault-fetch-directory', kwargs={'dir_id': dir_id}) return res @api_route(r'/vault/directory/(?P[a-fA-F0-9]+)/raw/', 'vault-fetch-directory') -@api_doc.route('/vault/directory/raw/', tags=['hidden'], handle_response=True) -@api_doc.arg('dir_id', - default='d4a96ba891017d0d26c15e509b4e6515e40d75ee', - argtype=api_doc.argtypes.sha1_git, - argdoc="The directory's sha1 identifier") -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.octet_stream, - retdoc='the cooked directory tarball') +@api_doc('/vault/directory/raw/', tags=['hidden'], handle_response=True) def api_vault_fetch_directory(request, dir_id): - """Fetch the archive of the directoy identified by dir_id.""" + """ + .. http:get:: /api/1/vault/directory/(dir_id)/raw/ + + Fetch the cooked archive for a directory. + + See :http:get:`/api/1/vault/directory/(dir_id)/` to get more + details on directory cooking. + + :param string dir_id: the directory's sha1 identifier + + :resheader Content-Type: application/octet-stream + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid directory identifier has been provided + :statuscode 404: requested directory can not be found in the SWH archive + """ # noqa _, obj_id = query.parse_hash_with_algorithms_or_throws( dir_id, ['sha1'], 'Only sha1_git is supported.') res = api_lookup( service.vault_fetch, 'directory', obj_id, notfound_msg="Directory with ID '{}' not found.".format(dir_id)) fname = '{}.tar.gz'.format(dir_id) response = HttpResponse(res, content_type='application/gzip') response['Content-disposition'] = 'attachment; filename={}'.format(fname) return response @api_route(r'/vault/revision/(?P[a-fA-F0-9]+)/gitfast/', 'vault-cook-revision_gitfast', methods=['GET', 'POST'], throttle_scope='swh_vault_cooking') @never_cache -@api_doc.route('/vault/revision/gitfast/', tags=['hidden']) -@api_doc.arg('rev_id', - default='9174026cfe69d73ef80b27890615f8b2ef5c265a', - argtype=api_doc.argtypes.sha1_git, - argdoc="The revision's sha1_git identifier") -@api_doc.param('email', default=None, - argtype=api_doc.argtypes.int, - doc="e-mail to notify when the bundle is ready") -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.dict, - retdoc='dictionary mapping containing the status of ' - 'the cooking') +@api_doc('/vault/revision/gitfast/', tags=['hidden']) def api_vault_cook_revision_gitfast(request, rev_id): - """Requests an archive of the revision identified by rev_id. + """ + .. http:get:: /api/1/vault/revision/(rev_id)/gitfast/ + .. http:post:: /api/1/vault/revision/(rev_id)/gitfast/ - To import the revision in the current directory, use:: + Request the cooking of a gitfast archive for a revision or check + its cooking status. - $ git init - $ zcat path/to/revision.gitfast.gz | git fast-import - $ git checkout HEAD - """ + That endpoint enables to create a vault cooking task for a revision + through a POST request or check the status of a previously created one + through a GET request. + + To import the revision in the current directory, use:: + + $ git init + $ zcat path/to/revision.gitfast.gz | git fast-import + $ git checkout HEAD + + :param string rev_id: the revision's sha1 identifier + + :query string email: e-mail to notify when the gitfast archive is ready + + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json string fetch_url: the url from which to download the archive once it has been cooked + (see :http:get:`/api/1/vault/revision/(rev_id)/gitfast/raw/`) + :>json string obj_type: the type of object to cook (directory or revision) + :>json string progress_message: message describing the cooking task progress + :>json number id: the cooking task id + :>json string status: the cooking task status (new/pending/done/failed) + :>json string obj_id: the identifier of the object to cook + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`post`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid revision identifier has been provided + :statuscode 404: requested revision can not be found in the SWH archive + """ # noqa _, obj_id = query.parse_hash_with_algorithms_or_throws( rev_id, ['sha1'], 'Only sha1_git is supported.') res = _dispatch_cook_progress(request, 'revision_gitfast', obj_id) res['fetch_url'] = reverse('vault-fetch-revision_gitfast', kwargs={'rev_id': rev_id}) return res -@api_route('/vault/revision/(?P[a-fA-F0-9]+)/gitfast/raw/', +@api_route(r'/vault/revision/(?P[a-fA-F0-9]+)/gitfast/raw/', 'vault-fetch-revision_gitfast') -@api_doc.route('/vault/revision/gitfast/raw/', tags=['hidden'], - handle_response=True) -@api_doc.arg('rev_id', - default='9174026cfe69d73ef80b27890615f8b2ef5c265a', - argtype=api_doc.argtypes.sha1_git, - argdoc="The revision's sha1_git identifier") -@api_doc.raises(exc=api_doc.excs.badinput, doc=doc_exc_bad_id) -@api_doc.raises(exc=api_doc.excs.notfound, doc=doc_exc_id_not_found) -@api_doc.returns(rettype=api_doc.rettypes.octet_stream, - retdoc='the cooked revision git fast-export') +@api_doc('/vault/revision/gitfast/raw/', tags=['hidden'], handle_response=True) def api_vault_fetch_revision_gitfast(request, rev_id): - """Fetch the archive of the revision identified by rev_id.""" + """ + .. http:get:: /api/1/vault/revision/(rev_id)/gitfast/raw/ + + Fetch the cooked gitfast archive for a revision. + + See :http:get:`/api/1/vault/revision/(rev_id)/gitfast/` to get more + details on directory cooking. + + :param string rev_id: the revision's sha1 identifier + + :resheader Content-Type: application/octet-stream + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid revision identifier has been provided + :statuscode 404: requested revision can not be found in the SWH archive + """ # noqa _, obj_id = query.parse_hash_with_algorithms_or_throws( rev_id, ['sha1'], 'Only sha1_git is supported.') res = api_lookup( service.vault_fetch, 'revision_gitfast', obj_id, notfound_msg="Revision with ID '{}' not found.".format(rev_id)) fname = '{}.gitfast.gz'.format(rev_id) response = HttpResponse(res, content_type='application/gzip') response['Content-disposition'] = 'attachment; filename={}'.format(fname) return response diff --git a/swh/web/assets/src/bundles/webapp/webapp.css b/swh/web/assets/src/bundles/webapp/webapp.css index b45db75f..35953d03 100644 --- a/swh/web/assets/src/bundles/webapp/webapp.css +++ b/swh/web/assets/src/bundles/webapp/webapp.css @@ -1,410 +1,392 @@ /** * Copyright (C) 2018 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ html { height: 100%; overflow-x: hidden; } body { min-height: 100%; margin: 0; position: relative; padding-bottom: 120px; } a { border-bottom-style: none; outline: none; } code { background-color: #f9f2f4; } pre code { background-color: transparent; } footer { background-color: #262626; color: #fff; font-size: 0.8rem; position: absolute; bottom: 0; width: 100%; padding-top: 20px; padding-bottom: 20px; z-index: 3000; } footer a, footer a:visited { color: #fecd1b; } footer a:hover { text-decoration: underline; } pre { background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; padding: 9.5px; font-size: 0.8rem; } .btn.active { background-color: #e7e7e7; } .card { margin-bottom: 5px !important; overflow-x: auto; } .navbar-brand { padding: 5px; margin-right: 0; } .table { margin-bottom: 0; } .swh-web-app-link a { text-decoration: none; outline: none; border: none; } .swh-web-app-link:hover { background-color: #efeff2; } .table > thead > tr > th { border-top: none; border-bottom: 1px solid #e20026; } .table > tbody > tr > td { border-style: none; } .sitename .first-word, .sitename .second-word { color: rgba(0, 0, 0, 0.75); font-weight: normal; font-size: 1.2rem; } .sitename .first-word { font-family: 'Alegreya Sans', sans-serif; } .sitename .second-word { font-family: 'Alegreya', serif; } -.swh-api-doc-route-upcoming > td, -.swh-api-doc-route-upcoming > td > a { - font-size: 90%; -} - -.swh-api-doc-route-deprecated > td, -.swh-api-doc-route-deprecated > td > a { - color: red; -} - -.swh-api-doc p { - margin-bottom: 0; -} - -.swh-api-doc dt { - text-align: right; -} - .swh-counter { font-size: 150%; } .swh-http-error { margin: 0 auto; text-align: center; } .swh-http-error-head { color: #2d353c; font-size: 30px; } .swh-http-error-code { bottom: 60%; color: #2d353c; font-size: 96px; line-height: 80px; margin-bottom: 10px !important; } .swh-http-error-desc { font-size: 12px; color: #647788; text-align: center; } .swh-http-error-desc pre { display: inline-block; text-align: left; max-width: 800px; white-space: pre-wrap; } .popover { max-width: 100%; z-index: 2000; } .modal { text-align: center; padding: 0 !important; } .modal::before { content: ''; display: inline-block; height: 100%; vertical-align: middle; margin-right: -4px; } .modal-dialog { display: inline-block; text-align: left; vertical-align: middle; } .dropdown-submenu { position: relative; } .dropdown-submenu .dropdown-menu { top: 0; left: -100%; margin-top: -5px; margin-left: -2px; } .dropdown-item:hover, .dropdown-item:focus { background-color: rgba(0, 0, 0, 0.1); } a.dropdown-left::before { content: "\f0d9"; font-family: 'FontAwesome'; display: block; width: 20px; height: 20px; float: left; margin-left: 0; } #swh-navbar { border-top-style: none; border-left-style: none; border-right-style: none; border-bottom: 5px solid; border-image: linear-gradient(to right, rgb(226, 0, 38) 0%, rgb(254, 205, 27) 100%) 1 1 1 1; width: 100%; padding: 5px; margin-bottom: 20px; margin-top: 30px; justify-content: normal; flex-wrap: nowrap; } #back-to-top { display: initial; position: fixed; bottom: 30px; right: 30px; z-index: 10; } #back-to-top a img { display: block; width: 32px; height: 32px; background-size: 32px 32px; text-indent: -999px; overflow: hidden; } .swh-top-bar { direction: ltr; height: 30px; position: fixed; top: 0; left: 0; width: 100%; z-index: 99999; background-color: #262626; color: #fff; text-align: center; font-size: 14px; } .swh-top-bar ul { margin-top: 4px; padding-left: 0; white-space: nowrap; } .swh-top-bar li { display: inline-block; margin-left: 10px; margin-right: 10px; } .swh-top-bar a, .swh-top-bar a:visited { color: white; } .swh-top-bar a.swh-current-site, .swh-top-bar a.swh-current-site:visited { color: #fecd1b; } .swh-donate-item { position: absolute; right: 0; } .swh-donate-link { border: 1px solid #fecd1b; background-color: #e20026; color: white !important; padding: 3px; border-radius: 3px; } .swh-navbar-content h4 { padding-top: 7px; } .swh-navbar-content .bread-crumbs { display: block; margin-left: -40px; } .swh-navbar-content .bread-crumbs li.bc-no-root { padding-top: 7px; } .main-sidebar { margin-top: 30px; } .content-wrapper { background: none; } .brand-image { max-height: 40px; } .brand-link { padding-top: 18.5px; padding-bottom: 18px; padding-left: 4px; border-bottom: 5px solid #e20026 !important; } .navbar-header a, ul.dropdown-menu a, ul.navbar-nav a, ul.nav-sidebar a { border-bottom-style: none; color: #323232; } .swh-sidebar .nav-link.active { color: #323232 !important; background-color: #e7e7e7 !important; } .swh-image-error { width: 80px; height: auto; } @media (max-width: 600px) { .swh-image-error { width: 40px; height: auto; } } .form-check-label { padding-top: 4px; } .swh-id-option { display: inline-block; margin-right: 5px; } .nav-pills .nav-link:not(.active):hover { color: rgba(0, 0, 0, 0.55); } .swh-heading-color { color: #e20026; } .sidebar-mini.sidebar-collapse .main-sidebar:hover { width: 4.6rem; } .sidebar-mini.sidebar-collapse .main-sidebar:hover .user-panel > .info, .sidebar-mini.sidebar-collapse .main-sidebar:hover .nav-sidebar .nav-link p, .sidebar-mini.sidebar-collapse .main-sidebar:hover .brand-text { visibility: hidden !important; } .sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info { transition: none; } .sidebar-mini.sidebar-mini.sidebar-collapse .sidebar { padding-right: 0; } .swh-words-logo { position: absolute; top: 0; left: 0; width: 73px; height: 73px; text-align: center; font-size: 10pt; color: rgba(0, 0, 0, 0.75); } .swh-words-logo:hover { text-decoration: none; } .swh-words-logo-swh { line-height: 1; padding-top: 13px; visibility: hidden; } \ No newline at end of file diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py index e9d58c63..92756cf9 100644 --- a/swh/web/common/utils.py +++ b/swh/web/common/utils.py @@ -1,284 +1,308 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +import docutils.parsers.rst +import docutils.utils import re from datetime import datetime, timezone from dateutil import parser as date_parser from dateutil import tz from django.core.cache import cache from django.core import urlresolvers from django.http import QueryDict from swh.model.exceptions import ValidationError from swh.model.identifiers import persistent_identifier from swh.web.common import service from swh.web.common.exc import BadInputExc def reverse(viewname, args=None, kwargs=None, query_params=None, current_app=None, urlconf=None): """An override of django reverse function supporting query parameters. Args: viewname: the name of the django view from which to compute a url args: list of url arguments ordered according to their position it kwargs: dictionary of url arguments indexed by their names query_params: dictionary of query parameters to append to the reversed url current_app: the name of the django app tighted to the view urlconf: url configuration module Returns: The url of the requested view with processed arguments and query parameters """ if kwargs: kwargs = {k: v for k, v in kwargs.items() if v is not None} url = urlresolvers.reverse( viewname, urlconf=urlconf, args=args, kwargs=kwargs, current_app=current_app) if query_params: query_params = {k: v for k, v in query_params.items() if v is not None} if query_params and len(query_params) > 0: query_dict = QueryDict('', mutable=True) for k in sorted(query_params.keys()): query_dict[k] = query_params[k] url += ('?' + query_dict.urlencode(safe='/')) return url def fmap(f, data): """Map f to data at each level. This must keep the origin data structure type: - map -> map - dict -> dict - list -> list - None -> None Args: f: function that expects one argument. data: data to traverse to apply the f function. list, map, dict or bare value. Returns: The same data-structure with modified values by the f function. """ if data is None: return data if isinstance(data, map): return map(lambda y: fmap(f, y), (x for x in data)) if isinstance(data, list): return [fmap(f, x) for x in data] if isinstance(data, dict): return {k: fmap(f, v) for (k, v) in data.items()} return f(data) def datetime_to_utc(date): """Returns datetime in UTC without timezone info Args: date (datetime.datetime): input datetime with timezone info Returns: datetime.datime: datetime in UTC without timezone info """ if date.tzinfo: return date.astimezone(tz.gettz('UTC')).replace(tzinfo=timezone.utc) else: return date def parse_timestamp(timestamp): """Given a time or timestamp (as string), parse the result as UTC datetime. Returns: a timezone-aware datetime representing the parsed value. None if the parsing fails. Samples: - 2016-01-12 - 2016-01-12T09:19:12+0100 - Today is January 1, 2047 at 8:21:00AM - 1452591542 """ if not timestamp: return None try: date = date_parser.parse(timestamp, ignoretz=False, fuzzy=True) return datetime_to_utc(date) except Exception: try: return datetime.utcfromtimestamp(float(timestamp)).replace( tzinfo=timezone.utc) except (ValueError, OverflowError) as e: raise BadInputExc(e) def shorten_path(path): """Shorten the given path: for each hash present, only return the first 8 characters followed by an ellipsis""" sha256_re = r'([0-9a-f]{8})[0-9a-z]{56}' sha1_re = r'([0-9a-f]{8})[0-9a-f]{32}' ret = re.sub(sha256_re, r'\1...', path) return re.sub(sha1_re, r'\1...', ret) def format_utc_iso_date(iso_date, fmt='%d %B %Y, %H:%M UTC'): """Turns a string reprensation of an ISO 8601 date string to UTC and format it into a more human readable one. For instance, from the following input string: '2017-05-04T13:27:13+02:00' the following one is returned: '04 May 2017, 11:27 UTC'. Custom format string may also be provided as parameter Args: iso_date (str): a string representation of an ISO 8601 date fmt (str): optional date formatting string Returns: A formatted string representation of the input iso date """ if not iso_date: return iso_date date = parse_timestamp(iso_date) return date.strftime(fmt) def gen_path_info(path): """Function to generate path data navigation for use with a breadcrumb in the swh web ui. For instance, from a path /folder1/folder2/folder3, it returns the following list:: [{'name': 'folder1', 'path': 'folder1'}, {'name': 'folder2', 'path': 'folder1/folder2'}, {'name': 'folder3', 'path': 'folder1/folder2/folder3'}] Args: path: a filesystem path Returns: A list of path data for navigation as illustrated above. """ path_info = [] if path: sub_paths = path.strip('/').split('/') path_from_root = '' for p in sub_paths: path_from_root += '/' + p path_info.append({'name': p, 'path': path_from_root.strip('/')}) return path_info def get_origin_visits(origin_info): """Function that returns the list of visits for a swh origin. That list is put in cache in order to speedup the navigation in the swh web browse ui. Args: origin_id (int): the id of the swh origin to fetch visits from Returns: A list of dict describing the origin visits:: [{'date': , 'origin': , 'status': <'full' | 'partial'>, 'visit': }, ... ] Raises: NotFoundExc if the origin is not found """ cache_entry_id = 'origin_%s_visits' % origin_info['id'] cache_entry = cache.get(cache_entry_id) if cache_entry: return cache_entry origin_visits = [] per_page = service.MAX_LIMIT last_visit = None while 1: visits = list(service.lookup_origin_visits(origin_info['id'], last_visit=last_visit, per_page=per_page)) origin_visits += visits if len(visits) < per_page: break else: if not last_visit: last_visit = per_page else: last_visit += per_page def _visit_sort_key(visit): ts = parse_timestamp(visit['date']).timestamp() return ts + (float(visit['visit']) / 10e3) for v in origin_visits: if 'metadata' in v: del v['metadata'] origin_visits = [dict(t) for t in set([tuple(d.items()) for d in origin_visits])] origin_visits = sorted(origin_visits, key=lambda v: _visit_sort_key(v)) cache.set(cache_entry_id, origin_visits) return origin_visits def get_swh_persistent_id(object_type, object_id, scheme_version=1): """ Returns the persistent identifier for a swh object based on: * the object type * the object id * the swh identifiers scheme version Args: object_type (str): the swh object type (content/directory/release/revision/snapshot) object_id (str): the swh object id (hexadecimal representation of its hash value) scheme_version (int): the scheme version of the swh persistent identifiers Returns: str: the swh object persistent identifier Raises: BadInputExc if the provided parameters do not enable to generate a valid identifier """ try: swh_id = persistent_identifier(object_type, object_id, scheme_version) except ValidationError as e: raise BadInputExc('Invalid object (%s) for swh persistent id. %s' % (object_id, e)) else: return swh_id + + +def parse_rst(text, report_level=2): + """ + Parse a reStructuredText string with docutils. + + Args: + text (str): string with reStructuredText markups in it + report_level (int): level of docutils report messages to print + (1 info 2 warning 3 error 4 severe 5 none) + + Returns: + docutils.nodes.document: a parsed docutils document + """ + parser = docutils.parsers.rst.Parser() + components = (docutils.parsers.rst.Parser,) + settings = docutils.frontend.OptionParser( + components=components).get_default_values() + settings.report_level = report_level + document = docutils.utils.new_document('rst-doc', settings=settings) + parser.parse(text, document) + return document diff --git a/swh/web/doc_config.py b/swh/web/doc_config.py index b6416bd6..02a4b0bb 100644 --- a/swh/web/doc_config.py +++ b/swh/web/doc_config.py @@ -1,35 +1,66 @@ # Copyright (C) 2017-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +import os + from sphinxcontrib import httpdomain +from swh.docs.sphinx.conf import setup as orig_setup + +from sphinx.ext import autodoc + + +class SimpleDocumenter(autodoc.FunctionDocumenter): + """ + Custom autodoc directive to display a docstring unindented + and without function signature header. + """ + objtype = "simple" + + # do not indent the content + content_indent = "" + + # do not add a header to the docstring + def add_directive_header(self, sig): + pass + + _swh_web_base_url = 'https://archive.softwareheritage.org' _swh_web_api_endpoint = 'api' _swh_web_api_version = 1 _swh_web_api_url = '%s/%s/%s/' % (_swh_web_base_url, _swh_web_api_endpoint, _swh_web_api_version) _swh_web_browse_endpoint = 'browse' _swh_web_browse_url = '%s/%s/' % (_swh_web_base_url, _swh_web_browse_endpoint) +def setup(app): + orig_setup(app) + app.add_autodocumenter(SimpleDocumenter) + # set an environment variable indicating we are currently + # building the swh-web documentation + os.environ['SWH_WEB_DOC_BUILD'] = '1' + + def customize_sphinx_conf(sphinx_conf): """ Utility function used to customize the sphinx doc build for swh-web globally (when building doc from swh-docs) or locally (when building doc from swh-web). Args: sphinx_conf (module): a reference to the sphinx conf.py module used to build the doc. """ # fix for sphinxcontrib.httpdomain 1.3 if 'Link' not in httpdomain.HEADER_REFS: httpdomain.HEADER_REFS['Link'] = httpdomain.IETFRef(5988, '5') sphinx_conf.extlinks['swh_web'] = (_swh_web_base_url + '/%s', None) sphinx_conf.extlinks['swh_web_api'] = (_swh_web_api_url + '%s', None) sphinx_conf.extlinks['swh_web_browse'] = (_swh_web_browse_url + '%s', None) + sphinx_conf.setup = setup diff --git a/swh/web/templates/apidoc.html b/swh/web/templates/apidoc.html index de129272..f79b71de 100644 --- a/swh/web/templates/apidoc.html +++ b/swh/web/templates/apidoc.html @@ -1,159 +1,182 @@ {% extends "layout.html" %} {% comment %} Copyright (C) 2015-2018 The Software Heritage developers See the AUTHORS file at the top-level directory of this distribution License: GNU Affero General Public License version 3, or any later version See top-level LICENSE file for more information {% endcomment %} {% load swh_templatetags %} {% load render_bundle from webpack_loader %} {% block title %}{{ heading }} – Software Heritage API {% endblock %} {% block navbar-content %} {% endblock %} {% block content %} -{% if docstring %} -
+{% if description %} +

Description

- {{ docstring | safe_docstring_display | safe }} + {{ description | safe_docstring_display | safe }}
{% endif %} {% if response_data %} -
+

Request

{{ request.method }} {{ request.absolute_uri }}

Response

{% if status_code != 200 %}
Status Code
-
{{ status_code }}
+
{{ status_code }}
{% endif %} {% if headers_data %}
Headers
{% for header_name, header_value in headers_data.items %}
{{ header_name }} {{ header_value | urlize_header_links | safe }}
{% endfor %} {% endif %}
Body
-
-      {{ response_data | urlize_links_and_mails | safe }}
-    
+
{{ response_data | urlize_links_and_mails | safe }}
{% endif %}
{% if urls and urls|length > 0 %} -
+
{% for url in urls %} - + {% endfor %}
URL Allowed Methods
{{ url.rule }}{{ url.rule | safe_docstring_display | safe }} {{ url.methods | dictsort:0 | join:', ' }}

{% endif %} {% if args and args|length > 0 %} -
+

Arguments

{% for arg in args %} -
-
{{ arg.name }}: {{ arg.type }}
+
+
{{ arg.name }} ({{ arg.type }})
{{ arg.doc | safe_docstring_display | safe }}
{% endfor %}

{% endif %} {% if params and params|length > 0 %} -
-

Parameters

+
+

Query parameters

{% for param in params %} -
-
{{ param.name }}: {{ param.type }}
+
+
{{ param.name }} ({{ param.type }})
{{ param.doc | safe_docstring_display | safe }}
{% endfor %}

{% endif %} -{% if headers and headers|length > 0 %} -
-

Headers

- {% for header in headers %} -
-
{{ header.name }}: string
+{% if reqheaders and reqheaders|length > 0 %} +
+

Request headers

+ {% for header in reqheaders %} +
+
{{ header.name }}
{{ header.doc | safe_docstring_display | safe }}
{% endfor %}

{% endif %} -{% if returns and returns|length > 0 %} -
+{% if resheaders and resheaders|length > 0 %} +
+

Response headers

+ {% for header in resheaders %} +
+
{{ header.name }}
+
{{ header.doc | safe_docstring_display | safe }}
+
+ {% endfor %} +
+
+{% endif %} +{% if return_type %} +

Returns

- {% for return in returns %} -
-
{{ return.type }}
-
{{ return.doc | safe_docstring_display | safe }}
+
+
{{ return_type }}
+
+

+ {% if return_type == 'array' %} + an array of objects containing the following keys: + {% elif return_type == 'octet stream' %} + the raw data as an octet stream + {% else %} + an object containing the following keys: + {% endif %} + {{ returns_list | safe_docstring_display | safe }} +

+
- {% endfor %}

{% endif %} -{% if excs and excs|length > 0 %} -
-

Errors

- {% for exc in excs %} -
-
{{ exc.exc }}
-
{{ exc.doc | safe_docstring_display | safe }}
+{% if status_codes and status_codes|length > 0 %} +
+

HTTP status codes

+ {% for status in status_codes %} +
+
{{ status.code }}
+
{{ status.doc | safe_docstring_display | safe }}
{% endfor %}

{% endif %} {% if examples and examples|length > 0 %} -
+

Examples

{% for example in examples %} -
+
{{ example }}
{% endfor %}
{% endif %} {% endblock %} diff --git a/swh/web/templates/browse-help.html b/swh/web/templates/browse-help.html index cd332ef0..a13f768e 100644 --- a/swh/web/templates/browse-help.html +++ b/swh/web/templates/browse-help.html @@ -1,189 +1,189 @@ {% extends "browse-layout.html" %} {% comment %} Copyright (C) 2017-2018 The Software Heritage developers See the AUTHORS file at the top-level directory of this distribution License: GNU Affero General Public License version 3, or any later version See top-level LICENSE file for more information {% endcomment %} {% block navbar-content %}

How to browse the archive ?

{% endblock %} {% block browse-content %}

Overview

This web application aims to provide HTML views to easily navigate in the Software Heritage archive. This is an ongoing development and new features and improvements will be progressively added over time.

URI scheme

The current URI scheme of that web application is described below and depends on the type of Software Heritage object to browse. Its exhaustive documentation can be consulted from the official Software Heritage development documentation

Context-independent browsing

Context-independent URLs provide information about objects (e.g., revisions, directories, contents, persons, …), independently of the contexts where they have been found (e.g., specific software origins, branches, commits, …).

Below are some examples of endpoints used to just render the corresponding information for user consumption:

Where hyperlinks are created when browsing these kind of endpoints, they always point to other context-independent browsing URLs.

Context-dependent browsing

Context-dependent URLs provide information about objects, limited to specific contexts where the objects have been found.

Currently, browsing the Software Heritage objects in the context of an origin is available. Below are some examples of such endpoints:

Search software origins to browse

In order to facilitate the browsing of the archive and generate relevant entry points to it, a search interface is available. Currently, it enables to search software origins from the URLs they were retrieved from. More search criteria will be added in the future. -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/swh/web/tests/api/test_apidoc.py b/swh/web/tests/api/test_apidoc.py index 8aef7a01..428dda8c 100644 --- a/swh/web/tests/api/test_apidoc.py +++ b/swh/web/tests/api/test_apidoc.py @@ -1,122 +1,288 @@ # Copyright (C) 2015-2018 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from nose.tools import istest, nottest from rest_framework.test import APITestCase from rest_framework.response import Response -from swh.web.api import apidoc +from swh.web.api.apidoc import api_doc, _parse_httpdomain_doc from swh.web.api.apiurls import api_route from swh.web.tests.testbase import SWHWebTestBase +# flake8: noqa -class APIDocTestCase(SWHWebTestBase, APITestCase): +httpdomain_doc = """ +.. http:get:: /api/1/revision/(sha1_git)/ - def setUp(self): + Get information about a revision in the SWH archive. + Revisions are identified by *sha1* checksums, compatible with Git commit identifiers. + See :func:`swh.model.identifiers.revision_identifier` in our data model module for details + about how they are computed. - self.arg_dict = { - 'name': 'my_pretty_arg', - 'default': 'some default value', - 'type': apidoc.argtypes.sha1, - 'doc': 'this arg does things' - } - self.stub_excs = [{'exc': apidoc.excs.badinput, - 'doc': 'My exception documentation'}] - self.stub_args = [{'name': 'stub_arg', - 'default': 'some_default'}] - self.stub_rule_list = [ - {'rule': 'some/route/with/args/', - 'methods': {'GET', 'HEAD', 'OPTIONS'}}, - {'rule': 'some/doc/route/', - 'methods': {'GET', 'HEAD', 'OPTIONS'}}, - {'rule': 'some/other/route/', - 'methods': {'GET', 'HEAD', 'OPTIONS'}} - ] - self.stub_return = { - 'type': apidoc.rettypes.dict.value, - 'doc': 'a dict with amazing properties' - } + :param string sha1_git: hexadecimal representation of the revision *sha1_git* identifier - @staticmethod - @apidoc.route('/my/nodoc/url/') - @nottest - def apidoc_nodoc_tester(request, arga=0, argb=0): - return Response(arga + argb) + :reqheader Accept: the requested response content type, + either *application/json* (default) or *application/yaml* + :resheader Content-Type: this depends on :http:header:`Accept` header of request + + :>json object author: information about the author of the revision + :>json string author_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the author of the revision + :>json object committer: information about the committer of the revision + :>json string committer_url: link to :http:get:`/api/1/person/(person_id)/` to get + information about the committer of the revision + :>json string committer_date: ISO representation of the commit date (in UTC) + :>json string date: ISO representation of the revision date (in UTC) + :>json string directory: the unique identifier that revision points to + :>json string directory_url: link to :http:get:`/api/1/directory/(sha1_git)/[(path)/]` + to get information about the directory associated to the revision + :>json string id: the revision unique identifier + :>json boolean merge: whether or not the revision corresponds to a merge commit + :>json string message: the message associated to the revision + :>json array parents: the parents of the revision, i.e. the previous revisions + that head directly to it, each entry of that array contains an unique parent + revision identifier but also a link to :http:get:`/api/1/revision/(sha1_git)/` + to get more informations about it + :>json string type: the type of the revision + + **Allowed HTTP Methods:** :http:method:`get`, :http:method:`head`, :http:method:`options` + + :statuscode 200: no error + :statuscode 400: an invalid *sha1_git* value has been provided + :statuscode 404: requested revision can not be found in the SWH archive + + **Request:** + + .. parsed-literal:: + + $ curl -i :swh_web_api:`revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/` +""" + + +class APIDocTestCase(SWHWebTestBase, APITestCase): @istest def apidoc_nodoc_failure(self): with self.assertRaises(Exception): - self.client.get('/api/1/my/nodoc/url/') + @api_doc('/my/nodoc/url/') + def apidoc_nodoc_tester(request, arga=0, argb=0): + return Response(arga + argb) @staticmethod @api_route(r'/some/(?P[0-9]+)/(?P[0-9]+)/', 'some-doc-route') - @apidoc.route('/some/doc/route/') + @api_doc('/some/doc/route/') @nottest def apidoc_route_tester(request, myarg, myotherarg, akw=0): """ Sample doc """ return {'result': int(myarg) + int(myotherarg) + akw} @istest def apidoc_route_doc(self): # when rv = self.client.get('/api/1/some/doc/route/') # then self.assertEqual(rv.status_code, 200) self.assertTemplateUsed('apidoc.html') @istest def apidoc_route_fn(self): # when rv = self.client.get('/api/1/some/1/1/') # then self.assertEqual(rv.status_code, 200) @staticmethod @api_route(r'/some/full/(?P[0-9]+)/(?P[0-9]+)/', 'some-complete-doc-route') - @apidoc.route('/some/complete/doc/route/') - @apidoc.arg('myarg', - default=67, - argtype=apidoc.argtypes.int, - argdoc='my arg') - @apidoc.arg('myotherarg', - default=42, - argtype=apidoc.argtypes.int, - argdoc='my other arg') - @apidoc.param('limit', argtype=apidoc.argtypes.int, default=10, - doc='Result limitation') - @apidoc.header('Link', doc='Header link returns for pagination purpose') - @apidoc.raises(exc=apidoc.excs.badinput, doc='Oops') - @apidoc.returns(rettype=apidoc.rettypes.dict, - retdoc='sum of args') + @api_doc('/some/complete/doc/route/') @nottest def apidoc_full_stack_tester(request, myarg, myotherarg, akw=0): """ Sample doc """ return {'result': int(myarg) + int(myotherarg) + akw} @istest def apidoc_full_stack_doc(self): # when rv = self.client.get('/api/1/some/complete/doc/route/') # then self.assertEqual(rv.status_code, 200) self.assertTemplateUsed('apidoc.html') @istest def apidoc_full_stack_fn(self): # when rv = self.client.get('/api/1/some/full/1/1/') # then self.assertEqual(rv.status_code, 200) + + @istest + def test_api_doc_parse_httpdomain(self): + doc_data = { + 'description': '', + 'urls': [], + 'args': [], + 'params': [], + 'resheaders': [], + 'reqheaders': [], + 'return_type': '', + 'returns': [], + 'status_codes': [], + 'examples': [] + } + + _parse_httpdomain_doc(httpdomain_doc, doc_data) + + expected_urls = [{ + 'rule': '/api/1/revision/ **\\(sha1_git\\)** /', + 'methods': ['GET', 'HEAD', 'OPTIONS'] + }] + + self.assertIn('urls', doc_data) + self.assertEqual(doc_data['urls'], expected_urls) + + expected_description = 'Get information about a revision in the SWH archive. \ +Revisions are identified by *sha1* checksums, compatible with Git commit \ +identifiers. See **swh.model.identifiers.revision_identifier** in our data \ +model module for details about how they are computed.' + self.assertIn('description', doc_data) + self.assertEqual(doc_data['description'], expected_description) + + expected_args = [{ + 'name': 'sha1_git', + 'type': 'string', + 'doc': 'hexadecimal representation of the revision *sha1_git* identifier' + }] + + self.assertIn('args', doc_data) + self.assertEqual(doc_data['args'], expected_args) + + expected_params = [] + self.assertIn('params', doc_data) + self.assertEqual(doc_data['params'], expected_params) + + expected_reqheaders = [{ + 'doc': 'the requested response content type, either *application/json* or *application/yaml*', + 'name': 'Accept' + }] + + self.assertIn('reqheaders', doc_data) + self.assertEqual(doc_data['reqheaders'], expected_reqheaders) + + expected_resheaders = [{ + 'doc': 'this depends on **Accept** header of request', + 'name': 'Content-Type' + }] + + self.assertIn('resheaders', doc_data) + self.assertEqual(doc_data['resheaders'], expected_resheaders) + + expected_statuscodes = [ + { + 'code': '200', + 'doc': 'no error' + }, + { + 'code': '400', + 'doc': 'an invalid *sha1_git* value has been provided' + }, + { + 'code': '404', + 'doc': 'requested revision can not be found in the SWH archive' + } + ] + + self.assertIn('status_codes', doc_data) + self.assertEqual(doc_data['status_codes'], expected_statuscodes) + + expected_return_type = 'object' + + self.assertIn('return_type', doc_data) + self.assertEqual(doc_data['return_type'], expected_return_type) + + expected_returns = [ + { + 'name': 'author', + 'type': 'object', + 'doc': 'information about the author of the revision' + }, + { + 'name': 'author_url', + 'type': 'string', + 'doc': 'link to ``_ to get information about the author of the revision' + }, + { + 'name': 'committer', + 'type': 'object', + 'doc': 'information about the committer of the revision' + }, + { + 'name': 'committer_url', + 'type': 'string', + 'doc': 'link to ``_ to get information about the committer of the revision' + }, + { + 'name': 'committer_date', + 'type': 'string', + 'doc': 'ISO representation of the commit date (in UTC)' + }, + { + 'name': 'date', + 'type': 'string', + 'doc': 'ISO representation of the revision date (in UTC)' + }, + { + 'name': 'directory', + 'type': 'string', + 'doc': 'the unique identifier that revision points to' + }, + { + 'name': 'directory_url', + 'type': 'string', + 'doc': 'link to ``_ to get information about the directory associated to the revision' + }, + { + 'name': 'id', + 'type': 'string', + 'doc': 'the revision unique identifier' + }, + { + 'name': 'merge', + 'type': 'boolean', + 'doc': 'whether or not the revision corresponds to a merge commit' + }, + { + 'name': 'message', + 'type': 'string', + 'doc': 'the message associated to the revision' + }, + { + 'name': 'parents', + 'type': 'array', + 'doc': 'the parents of the revision, i.e. the previous revisions that head directly to it, each entry of that array contains an unique parent revision identifier but also a link to ``_ to get more informations about it' + }, + { + 'name': 'type', + 'type': 'string', + 'doc': 'the type of the revision' + } + ] + + self.assertIn('returns', doc_data) + self.assertEqual(doc_data['returns'], expected_returns) + + expected_examples = ['/api/1/revision/aafb16d69fd30ff58afdd69036a26047f3aebdc6/'] + + self.assertIn('examples', doc_data) + self.assertEqual(doc_data['examples'], expected_examples)