diff --git a/docs/source/_static/rest-api.yml b/docs/source/_static/rest-api.yml index f8ffb699..49f985e8 100644 --- a/docs/source/_static/rest-api.yml +++ b/docs/source/_static/rest-api.yml @@ -773,7 +773,7 @@ paths: /groups/{name}/users: post: - operationId: get-group-users + operationId: post-group-users summary: Add users to a group parameters: - $ref: "#/components/parameters/groupName" diff --git a/docs/source/conf.py b/docs/source/conf.py index 4b54ae18..87432343 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,6 +11,7 @@ from pathlib import Path from urllib.request import urlretrieve from docutils import nodes +from ruamel.yaml import YAML from sphinx.directives.other import SphinxDirective from sphinx.util import logging @@ -46,6 +47,10 @@ source_suffix = [".md"] # default_role let's use use `foo` instead of ``foo`` in rST default_role = "literal" +docs = Path(__file__).parent.parent.absolute() +docs_source = docs / "source" +rest_api_yaml = docs_source / "_static" / "rest-api.yml" + # -- MyST configuration ------------------------------------------------------ # ref: https://myst-parser.readthedocs.io/en/latest/configuration.html @@ -123,6 +128,43 @@ class HelpAllDirective(SphinxDirective): return [par] +class RestAPILinksDirective(SphinxDirective): + """Directive to populate link targets for the REST API + + The resulting nodes resolve xref targets, + but are not actually rendered in the final result + which is handled by a custom template. + """ + + hast_content = False + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = {} + + def run(self): + targets = [] + yaml = YAML(typ="safe") + with rest_api_yaml.open() as f: + api = yaml.load(f) + for path, path_spec in api["paths"].items(): + for method, operation in path_spec.items(): + operation_id = operation.get("operationId") + if not operation_id: + logger.warning(f"No operation id for {method} {path}") + continue + # 'id' is the id on the page (must match redoc anchor) + # 'name' is the name of the ref for use in our documents + target = nodes.target( + ids=[f"operation/{operation_id}"], + names=[f"rest-api-{operation_id}"], + ) + targets.append(target) + self.state.document.note_explicit_target(target, target) + + return targets + + templates_path = ["_templates"] @@ -147,6 +189,7 @@ def setup(app): app.add_css_file("custom.css") app.add_directive("jupyterhub-generate-config", ConfigDirective) app.add_directive("jupyterhub-help-all", HelpAllDirective) + app.add_directive("jupyterhub-rest-api-links", RestAPILinksDirective) # -- Read The Docs ----------------------------------------------------------- @@ -155,8 +198,7 @@ def setup(app): # pre-requisite steps for "make html" from here if needed. # if os.environ.get("READTHEDOCS"): - docs = os.path.dirname(os.path.dirname(__file__)) - subprocess.check_call(["make", "metrics", "scopes"], cwd=docs) + subprocess.check_call(["make", "metrics", "scopes"], cwd=str(docs)) # -- Spell checking ---------------------------------------------------------- @@ -211,7 +253,6 @@ linkcheck_ignore = [ "https://github.com/jupyterhub/jupyterhub/pull/", # too many PRs in changelog "https://github.com/jupyterhub/jupyterhub/compare/", # too many comparisons in changelog r"https?://(localhost|127.0.0.1).*", # ignore localhost references in auto-links - r".*/rest-api.html#.*", # ignore javascript-resolved internal rest-api links r"https://linux.die.net/.*", # linux.die.net seems to block requests from CI with 403 sometimes ] linkcheck_anchors_ignore = [ diff --git a/docs/source/explanation/database.md b/docs/source/explanation/database.md index 25c49dc6..1990da0d 100644 --- a/docs/source/explanation/database.md +++ b/docs/source/explanation/database.md @@ -82,7 +82,7 @@ Additionally, there is usually _very_ little load on the database itself. By far the most taxing activity on the database is the 'list all users' endpoint, primarily used by the [idle-culling service](https://github.com/jupyterhub/jupyterhub-idle-culler). Database-based optimizations have been added to make even these operations feasible for large numbers of users: -1. State filtering on [GET /hub/api/users?state=active](../reference/rest-api.html#/default/get_users){.external}, +1. State filtering on [GET /hub/api/users?state=active](rest-api-get-users), which limits the number of results in the query to only the relevant subset (added in JupyterHub 1.3), rather than all users. 2. [Pagination](api-pagination) of all list endpoints, allowing the request of a large number of resources to be more fairly balanced with other Hub activities across multiple requests (added in 2.0). diff --git a/docs/source/howto/rest.md b/docs/source/howto/rest.md index e27b0074..b7e2e880 100644 --- a/docs/source/howto/rest.md +++ b/docs/source/howto/rest.md @@ -202,7 +202,7 @@ Authorization header. ### Use requests Using the popular Python [requests](https://docs.python-requests.org) -library, an API GET request is made, and the request sends an API token for +library, an API GET request is made to [/users](rest-api-get-users), and the request sends an API token for authorization. The response contains information about the users, here's example code to make an API request for the users of a JupyterHub deployment ```python @@ -220,7 +220,7 @@ r.raise_for_status() users = r.json() ``` -This example provides a slightly more complicated request, yet the +This example provides a slightly more complicated request (to [/groups/formgrade-data301/users](rest-api-post-group-users)), yet the process is very similar: ```python @@ -254,7 +254,7 @@ provided by notebook servers managed by JupyterHub if it has the necessary `acce Pagination is available through the `offset` and `limit` query parameters on list endpoints, which can be used to return ideally sized windows of results. -Here's example code demonstrating pagination on the `GET /users` +Here's example code demonstrating pagination on the [`GET /users`](rest-api-get-users) endpoint to fetch the first 20 records. ```python @@ -353,12 +353,18 @@ hub: With that setting in place, a new named-server is activated like this: +```{parsed-literal} +[POST /api/users/:username/servers/:servername](rest-api-post-user-server-name) +``` + +e.g. + ```bash curl -X POST -H "Authorization: token " "http://127.0.0.1:8081/hub/api/users//servers/" curl -X POST -H "Authorization: token " "http://127.0.0.1:8081/hub/api/users//servers/" ``` -The same servers can be stopped by substituting `DELETE` for `POST` above. +The same servers can be [stopped](rest-api-delete-user-server-name) by substituting `DELETE` for `POST` above. ### Some caveats for using named-servers diff --git a/docs/source/reference/rest-api.md b/docs/source/reference/rest-api.md index 2dd79dd9..2a1db988 100644 --- a/docs/source/reference/rest-api.md +++ b/docs/source/reference/rest-api.md @@ -12,3 +12,14 @@ redoc_options: NOTE: The contents of this markdown file are not used, this page is entirely generated from `_templates/redoc.html` and `_static/rest-api.yml` + +REST API methods can be linked by their operationId in rest-api.yml, +prefixed with `rest-api-`, e.g. + +```markdown +you cat [GET /api/users](rest-api-get-users) +``` + +```{jupyterhub-rest-api-links} + +``` diff --git a/docs/source/reference/sharing.md b/docs/source/reference/sharing.md index 34fa9e1f..f725a145 100644 --- a/docs/source/reference/sharing.md +++ b/docs/source/reference/sharing.md @@ -75,10 +75,14 @@ You can only modify access to one server at a time. To grant access to a particular user, in addition to `shares`, the granter must have at least `read:user:name` permission for the target user (or `read:group:name` if it's a group). Send a POST request to `/api/shares/:username/:servername` to grant permissions. + +```{parsed-literal} +[POST /api/shares/:username/:servername](rest-api-post-shares-server) +``` + The JSON body should specify what permissions to grant and whom to grant them to: -``` -POST /api/shares/:username/:servername +```python { "scopes": [], "user": "username", # or: @@ -100,8 +104,8 @@ You can only modify access to one server at a time. Send a PATCH request to `/api/shares/:username/:servername` to revoke permissions. -``` -PATCH /api/shares/:username/:servername +```{parsed-literal} +[PATCH /api/shares/:username/:servername](rest-api-patch-shares-server) ``` The JSON body should specify the scopes to revoke @@ -121,16 +125,16 @@ If `scopes` is empty or unspecified, _all_ scopes are revoked from the target us A DELETE request will revoke all shared access permissions for the given server. -``` -DELETE /api/shares/:username/:servername +```{parsed-literal} +[DELETE /api/shares/:username/:servername](rest-api-delete-shares-server) ``` ### View shares for a server To view shares for a given server, you need the permission `read:shares` with the appropriate _server_ filter. -``` -GET /api/shares/:username/:servername +```{parsed-literal} +[GET /api/shares/:username/:servername](rest-api-get-shares-server) ``` This is a paginated endpoint, so responses has `items` as a list of Share models, and `_pagination` for information about retrieving all shares if there are many: @@ -158,34 +162,34 @@ This is a paginated endpoint, so responses has `items` as a list of Share models } ``` -see the [rest-api](rest-api) for full details of the response models. +see the [rest-api](rest-api-get-shares-server) for full details of the response models. ### View servers shared with user or group To review servers shared with a given user or group, you need the permission `read:users:shares` or `read:groups:shares` with the appropriate _user_ or _group_ filter. +```{parsed-literal} +[GET /api/users/:username/shared](rest-api-get-user-shared) ``` -GET /api/users/:username/shared - -# or - -GET /api/groups/:groupname/shared +or +```{parsed-literal} +[GET /api/groups/:groupname/shared](rest-api-get-group-shared) ``` These are paginated endpoints. ### Access permission for a single user on a single server +```{parsed-literal} +[GET /api/users/:username/shared/:ownername/:servername](rest-api-get-user-shared-server) ``` -GET /api/users/:username/shared/:ownername/:servername - -# or - -GET /api/groups/:groupname/shared/:ownername/:servername +or +```{parsed-literal} +[GET /api/groups/:groupname/shared/:ownername/:servername](rest-api-get-group-shared-server) ``` will return the _single_ Share info for the given user or group for the server specified by `ownername/servername`, @@ -197,14 +201,14 @@ To revoke sharing permissions from the perspective of the user or group being sh you need the permissions `users:shares` or `groups:shares` with the appropriate _user_ or _group_ filter. This allows users to 'leave' shared servers, without needing permission to manage the server's sharing permissions. +``` +[DELETE /api/users/:username/shared/:ownername/:servername](rest-api-delete-user-shared-server) ``` -DELETE /api/users/:username/shared/:ownername/:servername - -# or - -DELETE /api/groups/:groupname/shared/:ownername/:servername +or +``` +[DELETE /api/groups/:groupname/shared/:ownername/:servername](rest-api-delete-group-shared-server) ``` will revoke all permissions granted to the user or group for the specified server. @@ -237,7 +241,7 @@ A Share returned in the REST API has the following structure: where exactly one of `user` and `group` is not null and the other is null. -See the [rest-api](rest-api) for full details of the response models. +See the [rest-api](rest-api-get-shares-server) for full details of the response models. ## Share via invitation code @@ -259,8 +263,8 @@ Share codes are much like shares, except: To create a share code: -``` -POST /api/share-code/:username/:servername +```{parsed-literal} +[POST /api/share-code/:username/:servername](rest-api-post-share-code) ``` where the body should include the scopes to be granted and expiration. @@ -288,7 +292,7 @@ The response contains the code itself: } ``` -See the [rest-api](rest-api) for full details of the response models. +See the [rest-api](rest-api-post-share-code) for full details of the response models. ### Accepting sharing invitations @@ -308,8 +312,8 @@ you will need to contact the owner of the server to start it. You can see existing invitations for -``` -GET /hub/api/share-codes/:username/:servername +```{parsed-literal} +[GET /hub/api/share-codes/:username/:servername](rest-api-get-share-codes-server) ``` which produces a paginated list of share codes (_excluding_ the codes themselves, which are not stored by jupyterhub): @@ -373,14 +377,14 @@ and the `id` that can be used for revocation: } ``` -see the [rest-api](rest-api) for full details of the response models. +see the [rest-api](rest-api-get-share-codes-server) for full details of the response models. ### Revoking invitations If you've finished inviting users to a server, you can revoke all invitations with: -``` -DELETE /hub/api/share-codes/:username/:servername +```{parsed-literal} +[DELETE /hub/api/share-codes/:username/:servername](rest-api-delete-share-code) ``` or revoke a single invitation code: