Merge branch '21535-multi-wf-delete'
[arvados.git] / sdk / python / discovery2pydoc.py
index 9f7f87d988e64534f196ff087a366be21dd6e9ff..70a51371ace81e0850640d5e3394eb0d092a9fc3 100755 (executable)
@@ -49,8 +49,8 @@ _ALIASED_METHODS = frozenset([
 ])
 _DEPRECATED_NOTICE = '''
 
-!!! deprecated
-    This resource is deprecated in the Arvados API.
+.. WARNING:: Deprecated
+   This resource is deprecated in the Arvados API.
 '''
 _DEPRECATED_RESOURCES = frozenset([
     'Humans',
@@ -77,12 +77,19 @@ If you work with this raw object, the keys of the dictionary are documented
 below, along with their types. The `items` key maps to a list of matching
 `{cls_name}` objects.
 '''
-_MODULE_PYDOC = '''Arvados API client documentation skeleton
-
-This module documents the methods and return types provided by the Arvados API
-client. Start with `ArvadosAPIClient`, which documents the methods available
-from the API client objects constructed by `arvados.api`. The implementation is
-generated dynamically at runtime when the client object is built.
+_MODULE_PYDOC = '''Arvados API client reference documentation
+
+This module provides reference documentation for the interface of the
+Arvados API client, including method signatures and type information for
+returned objects. However, the functions in `arvados.api` will return
+different classes at runtime that are generated dynamically from the Arvados
+API discovery document. The classes in this module do not have any
+implementation, and you should not instantiate them in your code.
+
+If you're just starting out, `ArvadosAPIClient` documents the methods
+available from the client object. From there, you can follow the trail into
+resource methods, request objects, and finally the data dictionaries returned
+by the API server.
 '''
 _SCHEMA_PYDOC = '''
 
@@ -95,25 +102,62 @@ to list the specific keys you need. Refer to the API documentation for details.
 '''
 
 _MODULE_PRELUDE = '''
+import googleapiclient.discovery
+import googleapiclient.http
+import httplib2
 import sys
+from typing import Any, Dict, Generic, List, Optional, TypeVar
 if sys.version_info < (3, 8):
-    from typing import Any
     from typing_extensions import TypedDict
 else:
-    from typing import Any, TypedDict
+    from typing import TypedDict
+
+# ST represents an API response type
+ST = TypeVar('ST', bound=TypedDict)
 '''
+_REQUEST_CLASS = '''
+class ArvadosAPIRequest(googleapiclient.http.HttpRequest, Generic[ST]):
+    """Generic API request object
+
+    When you call an API method in the Arvados Python SDK, it returns a
+    request object. You usually call `execute()` on this object to submit the
+    request to your Arvados API server and retrieve the response. `execute()`
+    will return the type of object annotated in the subscript of
+    `ArvadosAPIRequest`.
+    """
+
+    def execute(self, http: Optional[httplib2.Http]=None, num_retries: int=0) -> ST:
+        """Execute this request and return the response
+
+        Arguments:
+
+        * http: httplib2.Http | None --- The HTTP client object to use to
+          execute the request. If not specified, uses the HTTP client object
+          created with the API client object.
+
+        * num_retries: int --- The maximum number of times to retry this
+          request if the server returns a retryable failure. The API client
+          object also has a maximum number of retries specified when it is
+          instantiated (see `arvados.api.api_client`). This request is run
+          with the larger of that number and this argument. Default 0.
+        """
 
-_TYPE_MAP = {
+'''
+
+# Annotation represents a valid Python type annotation. Future development
+# could expand this to include other valid types like `type`.
+Annotation = str
+_TYPE_MAP: Mapping[str, Annotation] = {
     # Map the API's JavaScript-based type names to Python annotations.
     # Some of these may disappear after Arvados issue #19795 is fixed.
-    'Array': 'list',
-    'array': 'list',
+    'Array': 'List',
+    'array': 'List',
     'boolean': 'bool',
     # datetime fields are strings in ISO 8601 format.
     'datetime': 'str',
-    'Hash': 'dict[str, Any]',
+    'Hash': 'Dict[str, Any]',
     'integer': 'int',
-    'object': 'dict[str, Any]',
+    'object': 'Dict[str, Any]',
     'string': 'str',
     'text': 'str',
 }
@@ -182,20 +226,30 @@ class Parameter(inspect.Parameter):
         if default_value is None:
             default_doc = ''
         else:
-            default_doc = f" Default {default_value!r}."
-        # If there is no description, use a zero-width space to help Markdown
-        # parsers retain the definition list structure.
-        description = self._spec['description'] or '\u200b'
+            default_doc = f"Default {default_value!r}."
+        description = self._spec['description']
+        doc_parts = [f'{self.api_name}: {self.annotation}']
+        if description or default_doc:
+            doc_parts.append('---')
+            if description:
+                doc_parts.append(description)
+            if default_doc:
+                doc_parts.append(default_doc)
         return f'''
-{self.api_name}: {self.annotation}
-: {description}{default_doc}
+* {' '.join(doc_parts)}
 '''
 
 
 class Method:
-    def __init__(self, name: str, spec: Mapping[str, Any]) -> None:
+    def __init__(
+            self,
+            name: str,
+            spec: Mapping[str, Any],
+            annotate: Callable[[Annotation], Annotation]=str,
+    ) -> None:
         self.name = name
         self._spec = spec
+        self._annotate = annotate
         self._required_params = []
         self._optional_params = []
         for param_name, param_spec in spec['parameters'].items():
@@ -217,7 +271,8 @@ class Method:
         try:
             returns = get_type_annotation(self._spec['response']['$ref'])
         except KeyError:
-            returns = 'dict[str, Any]'
+            returns = 'Dict[str, Any]'
+        returns = self._annotate(returns)
         return inspect.Signature(parameters, return_annotation=returns)
 
     def doc(self, doc_slice: slice=slice(None)) -> str:
@@ -281,7 +336,7 @@ def document_resource(name: str, spec: Mapping[str, Any]) -> str:
     if class_name in _DEPRECATED_RESOURCES:
         docstring += _DEPRECATED_NOTICE
     methods = [
-        Method(key, meth_spec)
+        Method(key, meth_spec, 'ArvadosAPIRequest[{}]'.format)
         for key, meth_spec in spec['methods'].items()
         if key not in _ALIASED_METHODS
     ]
@@ -350,7 +405,11 @@ def main(arglist: Optional[Sequence[str]]=None) -> int:
     for name, resource_spec in resources:
         print(document_resource(name, resource_spec), file=args.out_file)
 
-    print('''class ArvadosAPIClient:''', file=args.out_file)
+    print(
+        _REQUEST_CLASS,
+        '''class ArvadosAPIClient(googleapiclient.discovery.Resource):''',
+        sep='\n', file=args.out_file,
+    )
     for name, _ in resources:
         class_name = classify_name(name)
         docstring = f"Return an instance of `{class_name}` to call methods via this client"