add rest-api-{operation} xref targets, so we can link to our own REST API

This commit is contained in:
Min RK
2024-02-22 10:57:36 +01:00
parent 3286afd848
commit b7dffc7afc
6 changed files with 104 additions and 42 deletions

View File

@@ -773,7 +773,7 @@ paths:
/groups/{name}/users: /groups/{name}/users:
post: post:
operationId: get-group-users operationId: post-group-users
summary: Add users to a group summary: Add users to a group
parameters: parameters:
- $ref: "#/components/parameters/groupName" - $ref: "#/components/parameters/groupName"

View File

@@ -11,6 +11,7 @@ from pathlib import Path
from urllib.request import urlretrieve from urllib.request import urlretrieve
from docutils import nodes from docutils import nodes
from ruamel.yaml import YAML
from sphinx.directives.other import SphinxDirective from sphinx.directives.other import SphinxDirective
from sphinx.util import logging 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 let's use use `foo` instead of ``foo`` in rST
default_role = "literal" default_role = "literal"
docs = Path(__file__).parent.parent.absolute()
docs_source = docs / "source"
rest_api_yaml = docs_source / "_static" / "rest-api.yml"
# -- MyST configuration ------------------------------------------------------ # -- MyST configuration ------------------------------------------------------
# ref: https://myst-parser.readthedocs.io/en/latest/configuration.html # ref: https://myst-parser.readthedocs.io/en/latest/configuration.html
@@ -123,6 +128,43 @@ class HelpAllDirective(SphinxDirective):
return [par] 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"] templates_path = ["_templates"]
@@ -147,6 +189,7 @@ def setup(app):
app.add_css_file("custom.css") app.add_css_file("custom.css")
app.add_directive("jupyterhub-generate-config", ConfigDirective) app.add_directive("jupyterhub-generate-config", ConfigDirective)
app.add_directive("jupyterhub-help-all", HelpAllDirective) app.add_directive("jupyterhub-help-all", HelpAllDirective)
app.add_directive("jupyterhub-rest-api-links", RestAPILinksDirective)
# -- Read The Docs ----------------------------------------------------------- # -- Read The Docs -----------------------------------------------------------
@@ -155,8 +198,7 @@ def setup(app):
# pre-requisite steps for "make html" from here if needed. # pre-requisite steps for "make html" from here if needed.
# #
if os.environ.get("READTHEDOCS"): if os.environ.get("READTHEDOCS"):
docs = os.path.dirname(os.path.dirname(__file__)) subprocess.check_call(["make", "metrics", "scopes"], cwd=str(docs))
subprocess.check_call(["make", "metrics", "scopes"], cwd=docs)
# -- Spell checking ---------------------------------------------------------- # -- Spell checking ----------------------------------------------------------
@@ -211,7 +253,6 @@ linkcheck_ignore = [
"https://github.com/jupyterhub/jupyterhub/pull/", # too many PRs in changelog "https://github.com/jupyterhub/jupyterhub/pull/", # too many PRs in changelog
"https://github.com/jupyterhub/jupyterhub/compare/", # too many comparisons 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"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 r"https://linux.die.net/.*", # linux.die.net seems to block requests from CI with 403 sometimes
] ]
linkcheck_anchors_ignore = [ linkcheck_anchors_ignore = [

View File

@@ -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). 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: 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. 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). 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).

View File

@@ -202,7 +202,7 @@ Authorization header.
### Use requests ### Use requests
Using the popular Python [requests](https://docs.python-requests.org) 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 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 ```python
@@ -220,7 +220,7 @@ r.raise_for_status()
users = r.json() 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: process is very similar:
```python ```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 Pagination is available through the `offset` and `limit` query parameters on
list endpoints, which can be used to return ideally sized windows of results. 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. endpoint to fetch the first 20 records.
```python ```python
@@ -353,12 +353,18 @@ hub:
With that setting in place, a new named-server is activated like this: 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 ```bash
curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/servers/<serverA>" curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/servers/<serverA>"
curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/servers/<serverB>" curl -X POST -H "Authorization: token <token>" "http://127.0.0.1:8081/hub/api/users/<user>/servers/<serverB>"
``` ```
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 ### Some caveats for using named-servers

View File

@@ -12,3 +12,14 @@ redoc_options:
NOTE: The contents of this markdown file are not used, NOTE: The contents of this markdown file are not used,
this page is entirely generated from `_templates/redoc.html` and `_static/rest-api.yml` 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}
```

View File

@@ -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). 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. 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: The JSON body should specify what permissions to grant and whom to grant them to:
``` ```python
POST /api/shares/:username/:servername
{ {
"scopes": [], "scopes": [],
"user": "username", # or: "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. Send a PATCH request to `/api/shares/:username/:servername` to revoke permissions.
``` ```{parsed-literal}
PATCH /api/shares/:username/:servername [PATCH /api/shares/:username/:servername](rest-api-patch-shares-server)
``` ```
The JSON body should specify the scopes to revoke 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. A DELETE request will revoke all shared access permissions for the given server.
``` ```{parsed-literal}
DELETE /api/shares/:username/:servername [DELETE /api/shares/:username/:servername](rest-api-delete-shares-server)
``` ```
### View shares for a server ### View shares for a server
To view shares for a given server, you need the permission `read:shares` with the appropriate _server_ filter. To view shares for a given server, you need the permission `read:shares` with the appropriate _server_ filter.
``` ```{parsed-literal}
GET /api/shares/:username/:servername [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: 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 ### 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. 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
# or
GET /api/groups/:groupname/shared
```{parsed-literal}
[GET /api/groups/:groupname/shared](rest-api-get-group-shared)
``` ```
These are paginated endpoints. These are paginated endpoints.
### Access permission for a single user on a single server ### 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
# or
GET /api/groups/:groupname/shared/:ownername/:servername
```{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`, 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. 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. 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
# or
DELETE /api/groups/:groupname/shared/:ownername/:servername
```
[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. 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. 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 ## Share via invitation code
@@ -259,8 +263,8 @@ Share codes are much like shares, except:
To create a share code: To create a share code:
``` ```{parsed-literal}
POST /api/share-code/:username/:servername [POST /api/share-code/:username/:servername](rest-api-post-share-code)
``` ```
where the body should include the scopes to be granted and expiration. 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 ### 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 You can see existing invitations for
``` ```{parsed-literal}
GET /hub/api/share-codes/:username/:servername [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): 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 ### Revoking invitations
If you've finished inviting users to a server, you can revoke all invitations with: If you've finished inviting users to a server, you can revoke all invitations with:
``` ```{parsed-literal}
DELETE /hub/api/share-codes/:username/:servername [DELETE /hub/api/share-codes/:username/:servername](rest-api-delete-share-code)
``` ```
or revoke a single invitation code: or revoke a single invitation code: