From 426f8e3ef0963f4053d03e231bb3ae5eadd95603 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 5 Oct 2022 14:02:04 +0300 Subject: [PATCH] Extend SDK layer 1 docs (#5011) --- .github/workflows/full.yml | 41 +- .github/workflows/github_pages.yml | 43 -- .github/workflows/main.yml | 100 +++- .vscode/launch.json | 15 + cvat-sdk/gen/design.md | 60 --- cvat-sdk/gen/generate.sh | 20 +- cvat-sdk/gen/postprocess.py | 56 +- cvat-sdk/gen/templates/README.md.template | 26 + .../openapi-generator/README.mustache | 2 +- .../openapi-generator/README_common.mustache | 8 +- .../openapi-generator/api_doc.mustache | 59 +++ .../api_doc_example.mustache | 44 ++ .../openapi-generator/model_doc.mustache | 32 ++ cvat/apps/engine/schema.py | 12 +- site/content/en/docs/api_sdk/sdk/_index.md | 4 +- .../en/docs/api_sdk/sdk/developer-guide.md | 77 ++- .../en/docs/api_sdk/sdk/lowlevel-api.md | 499 +++++++++--------- .../en/docs/api_sdk/sdk/reference/_index.md | 6 + .../api_sdk/sdk/reference/apis/.gitignore | 2 + .../api_sdk/sdk/reference/models/.gitignore | 3 + .../api_sdk/sdk/reference/models/_index.md | 6 + site/process_sdk_docs.py | 295 +++++++++++ site/requirements.txt | 5 +- .../sdk/reference/apis/_index.md.template | 8 + 24 files changed, 997 insertions(+), 426 deletions(-) delete mode 100644 .github/workflows/github_pages.yml delete mode 100644 cvat-sdk/gen/design.md create mode 100644 cvat-sdk/gen/templates/README.md.template create mode 100644 cvat-sdk/gen/templates/openapi-generator/api_doc.mustache create mode 100644 cvat-sdk/gen/templates/openapi-generator/api_doc_example.mustache create mode 100644 cvat-sdk/gen/templates/openapi-generator/model_doc.mustache create mode 100644 site/content/en/docs/api_sdk/sdk/reference/_index.md create mode 100644 site/content/en/docs/api_sdk/sdk/reference/apis/.gitignore create mode 100644 site/content/en/docs/api_sdk/sdk/reference/models/.gitignore create mode 100644 site/content/en/docs/api_sdk/sdk/reference/models/_index.md create mode 100755 site/process_sdk_docs.py create mode 100644 site/templates/en/docs/api_sdk/sdk/reference/apis/_index.md.template diff --git a/.github/workflows/full.yml b/.github/workflows/full.yml index 96651d74a..321b08c19 100644 --- a/.github/workflows/full.yml +++ b/.github/workflows/full.yml @@ -64,12 +64,13 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - name: Create image directory + - name: Create artifact directories run: | mkdir /tmp/cvat_server mkdir /tmp/cvat_ui + mkdir /tmp/cvat_sdk - - name: CVAT server. Build + - name: CVAT server. Build and push uses: docker/build-push-action@v3 with: cache-from: type=local,src=/tmp/cvat_cache_server @@ -78,7 +79,7 @@ jobs: tags: cvat/server outputs: type=docker,dest=/tmp/cvat_server/image.tar - - name: CVAT UI. Build + - name: CVAT UI. Build and push uses: docker/build-push-action@v3 with: cache-from: type=local,src=/tmp/cvat_cache_ui @@ -87,6 +88,18 @@ jobs: tags: cvat/ui outputs: type=docker,dest=/tmp/cvat_ui/image.tar + - name: CVAT SDK. Build + run: | + docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \ + --entrypoint /bin/bash -u root cvat/server \ + -c 'python manage.py spectacular --file /transfer/schema.yml' + pip3 install --user -r cvat-sdk/gen/requirements.txt + cd cvat-sdk/ + gen/generate.sh + cd .. + + cp -r cvat-sdk/* /tmp/cvat_sdk/ + - name: Upload CVAT server artifact uses: actions/upload-artifact@v3 with: @@ -99,6 +112,12 @@ jobs: name: cvat_ui path: /tmp/cvat_ui/image.tar + - name: Upload CVAT SDK artifact + uses: actions/upload-artifact@v3 + with: + name: cvat_sdk + path: /tmp/cvat_sdk/ + rest_api: needs: build runs-on: ubuntu-latest @@ -158,6 +177,12 @@ jobs: name: cvat_ui path: /tmp/cvat_ui/ + - name: Download CVAT SDK package + uses: actions/download-artifact@v3 + with: + name: cvat_sdk + path: /tmp/cvat_sdk/ + - name: Load Docker images run: | docker load --input /tmp/cvat_server/image.tar @@ -168,15 +193,7 @@ jobs: - name: Running REST API and SDK tests run: | - docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \ - --entrypoint /bin/bash -u root cvat/server \ - -c 'python manage.py spectacular --file /transfer/schema.yml' - pip3 install --user -r cvat-sdk/gen/requirements.txt - cd cvat-sdk/ - gen/generate.sh - cd .. - - pip3 install --user cvat-sdk/ + pip3 install --user /tmp/cvat_sdk/ pip3 install --user cvat-cli/ pip3 install --user -r tests/python/requirements.txt pytest tests/python -s -v diff --git a/.github/workflows/github_pages.yml b/.github/workflows/github_pages.yml deleted file mode 100644 index 6c3d58da3..000000000 --- a/.github/workflows/github_pages.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Github pages - -on: - push: - branches: - - develop - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: recursive - fetch-depth: 0 - - - name: Setup Hugo - uses: peaceiris/actions-hugo@v2 - with: - hugo-version: '0.83.1' - extended: true - - - name: Setup Node - uses: actions/setup-node@v2 - with: - node-version: '16.x' - - - name: Install npm packages - working-directory: ./site - run: | - npm ci - - - name: Build docs - run: | - pip install -r site/requirements.txt - python site/build_docs.py - - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./public - force_orphan: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e4547d0fd..c50af7a6e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,10 +65,11 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - name: Create image directory + - name: Create artifact directories run: | mkdir /tmp/cvat_server mkdir /tmp/cvat_ui + mkdir /tmp/cvat_sdk - name: CVAT server. Build and push uses: docker/build-push-action@v3 @@ -88,6 +89,18 @@ jobs: tags: cvat/ui outputs: type=docker,dest=/tmp/cvat_ui/image.tar + - name: CVAT SDK. Build + run: | + docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \ + --entrypoint /bin/bash -u root cvat/server \ + -c 'python manage.py spectacular --file /transfer/schema.yml' + pip3 install --user -r cvat-sdk/gen/requirements.txt + cd cvat-sdk/ + gen/generate.sh + cd .. + + cp -r cvat-sdk/* /tmp/cvat_sdk/ + - name: Upload CVAT server artifact uses: actions/upload-artifact@v3 with: @@ -100,7 +113,13 @@ jobs: name: cvat_ui path: /tmp/cvat_ui/image.tar - rest_api: + - name: Upload CVAT SDK artifact + uses: actions/upload-artifact@v3 + with: + name: cvat_sdk + path: /tmp/cvat_sdk/ + + rest_api_testing: needs: build runs-on: ubuntu-latest steps: @@ -125,6 +144,12 @@ jobs: name: cvat_ui path: /tmp/cvat_ui/ + - name: Download CVAT SDK package + uses: actions/download-artifact@v3 + with: + name: cvat_sdk + path: /tmp/cvat_sdk/ + - name: Load Docker images run: | docker load --input /tmp/cvat_server/image.tar @@ -135,22 +160,14 @@ jobs: - name: Running REST API tests run: | - docker run --rm -v ${PWD}/cvat-sdk/schema/:/transfer \ - --entrypoint /bin/bash -u root cvat/server \ - -c 'python manage.py spectacular --file /transfer/schema.yml' - pip3 install --user -r cvat-sdk/gen/requirements.txt - cd cvat-sdk/ - gen/generate.sh - cd .. - - pip3 install --user cvat-sdk/ + pip3 install --user /tmp/cvat_sdk/ pip3 install --user -r tests/python/requirements.txt pytest tests/python/rest_api -k 'GET' -s - name: Creating a log file from cvat containers if: failure() env: - LOGS_DIR: "${{ github.workspace }}/rest_api" + LOGS_DIR: "${{ github.workspace }}/rest_api_testing" run: | mkdir $LOGS_DIR docker logs test_cvat_server_1 > $LOGS_DIR/cvat.log @@ -161,7 +178,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: container_logs - path: "${{ github.workspace }}/rest_api" + path: "${{ github.workspace }}/rest_api_testing" unit_testing: needs: build @@ -320,9 +337,64 @@ jobs: name: cypress_screenshots_${{ matrix.specs }} path: ${{ github.workspace }}/tests/cypress/screenshots + generate_github_pages: + if: github.ref == 'refs/heads/develop' + needs: [rest_api_testing, unit_testing, e2e_testing] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + fetch-depth: 0 + + - name: Download CVAT server images + uses: actions/download-artifact@v3 + with: + name: cvat_server + path: /tmp/cvat_server/ + + - name: Download CVAT server images + uses: actions/download-artifact@v3 + with: + name: cvat_sdk + path: /tmp/cvat_sdk/ + + - name: Load Docker images + run: | + docker load --input /tmp/cvat_server/image.tar + + - name: Setup Hugo + uses: peaceiris/actions-hugo@v2 + with: + hugo-version: '0.83.1' + extended: true + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: '16.x' + + - name: Install npm packages + working-directory: ./site + run: | + npm ci + + - name: Build docs + run: | + pip install -r site/requirements.txt + python site/process_sdk_docs.py --input-dir /tmp/cvat_sdk/docs/ --site-root site/ + python site/build_docs.py + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./public + force_orphan: true + publish_dev_images: if: github.ref == 'refs/heads/develop' - needs: [rest_api, unit_testing, e2e_testing] + needs: [rest_api_testing, unit_testing, e2e_testing, generate_github_pages] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.vscode/launch.json b/.vscode/launch.json index 7c9e55bac..6517d0db8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -252,6 +252,21 @@ "cwd": "${workspaceFolder}", "console": "integratedTerminal" }, + { + "name": "sdk docs: Postprocess generated docs", + "type": "python", + "request": "launch", + "justMyCode": false, + "stopOnEntry": false, + "python": "${command:python.interpreterPath}", + "program": "${workspaceFolder}/site/process_sdk_docs.py", + "args": [ + "--input-dir", "${workspaceFolder}/cvat-sdk/docs/", + "--site-root", "${workspaceFolder}/site/", + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal" + }, { "name": "server: Generate REST API Schema", "type": "python", diff --git a/cvat-sdk/gen/design.md b/cvat-sdk/gen/design.md deleted file mode 100644 index 360c7345c..000000000 --- a/cvat-sdk/gen/design.md +++ /dev/null @@ -1,60 +0,0 @@ -# API design decisions - -Generated API is modified from what `openapi-generator` does by default. -Changes are mostly focused on better user experience - including better -usage patterns and simpler/faster ways to achieve results. - -## Changes - -- Added type annotations for return types and class members - This change required us to implement a custom post-processing script, - which converts generated types into correct type annotations. The types - generated by default are supposed to work with the API implementation - (parameter validation and parsing), but they are not applicable as - type annotations (they have incorrect syntax). Custom post-processing - allowed us to make these types correct type annotations. - Other possible solutions: - - There is a `python-experimental` API generator, which may solve - some issues, but it is unstable and requires python 3.9. Our API - works with 3.7, which is the lowest supported version now. - - Custom templates - partially works, but only in limited cases - (model fields). It's very hard to maintain the template code and - logic for this. Only `if` checks and `for` loops are available in - mustache templates, which is not enough for annotation generation. - -- Separate APIs are embedded into the general `APIClient` class - Now we have: - ```python - with ApiClient(config) as api_client: - result1 = api_client.foo_api.operation1() - result2 = api_client.bar_api.operation2() - ``` - - This showed to be more convenient than default: - ```python - with ApiClient(config) as api_client: - foo_api = FooApi(api_client) - result1 = foo_api.operation1() - result2 = foo_api.operation2() - - bar_api = BarApi(api_client) - result3 = bar_api.operation3() - result4 = bar_api.operation4() - ``` - - This also required custom post-processing. Operation Ids are - [supposed to be unique](https://swagger.io/specification/#operation-object) - in the OpenAPI / Swagger specification. Therefore, we can't generate such - schema on the server, nor we can't expect it to be supported in the - API generator. - -- Operations have IDs like `/_` - This also showed to be more readable and more natural than DRF-spectacular's - default `/_`. - -- Server operations have different types for input and output values - While it can be expected that an endopint with POST/PUT methods available - (like `create` or `partial_update`) has the same type for input and output - (because it looks natural), it also leads to the situation, in which there - are lots of read-/write-only fields, and it becomes hard for understanding. - This clear type separation is supposed to make it simpler for users. diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh index 1dc02a24a..903f65e9a 100755 --- a/cvat-sdk/gen/generate.sh +++ b/cvat-sdk/gen/generate.sh @@ -13,7 +13,7 @@ LIB_NAME="cvat_sdk" LAYER1_LIB_NAME="${LIB_NAME}/api_client" DST_DIR="." TEMPLATE_DIR="gen" -PYTHON_POST_PROCESS_FILE="${TEMPLATE_DIR}/postprocess.py" +POST_PROCESS_SCRIPT="${TEMPLATE_DIR}/postprocess.py" mkdir -p "${DST_DIR}/" rm -f -r "${DST_DIR}/docs" "${DST_DIR}/${LAYER1_LIB_NAME}" "requirements/" @@ -34,5 +34,19 @@ cp -r "${TEMPLATE_DIR}/templates/requirements" "${DST_DIR}/" cp -r "${TEMPLATE_DIR}/templates/MANIFEST.in" "${DST_DIR}/" mv "${DST_DIR}/requirements.txt" "${DST_DIR}/requirements/api_client.txt" -# Do custom postprocessing -"${PYTHON_POST_PROCESS_FILE}" --schema "schema/schema.yml" --input-path "${DST_DIR}/${LIB_NAME}" +# Do custom postprocessing for code files +"${POST_PROCESS_SCRIPT}" --schema "schema/schema.yml" --input-path "${DST_DIR}/${LIB_NAME}" + +# Do custom postprocessing for docs files +"${POST_PROCESS_SCRIPT}" --schema "schema/schema.yml" --input-path "${DST_DIR}/docs" --file-ext '.md' +"${POST_PROCESS_SCRIPT}" --schema "schema/schema.yml" --input-path "${DST_DIR}/README.md" + +API_DOCS_DIR="${DST_DIR}/docs/apis/" +MODEL_DOCS_DIR="${DST_DIR}/docs/models/" +mkdir "${API_DOCS_DIR}" +mkdir "${MODEL_DOCS_DIR}" +mv "${DST_DIR}/docs/"*Api.md "${API_DOCS_DIR}" +mv "${DST_DIR}/docs/"*.md "${MODEL_DOCS_DIR}" +mv "${DST_DIR}/README.md" "${DST_DIR}/docs/" + +cp "${TEMPLATE_DIR}/templates/README.md.template" "${DST_DIR}/README.md" diff --git a/cvat-sdk/gen/postprocess.py b/cvat-sdk/gen/postprocess.py index 8e2476a47..376ad2f35 100755 --- a/cvat-sdk/gen/postprocess.py +++ b/cvat-sdk/gen/postprocess.py @@ -1,13 +1,13 @@ #!/usr/bin/env python -# Copyright (C) 2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT +import argparse import os.path as osp import re import sys -from argparse import ArgumentParser from glob import glob from inflection import underscore @@ -29,7 +29,7 @@ def collect_operations(schema): return operations -class Processor: +class Replacer: REPLACEMENT_TOKEN = r"%%%" ARGS_TOKEN = r"!!!" @@ -77,7 +77,7 @@ class Processor: def _process_file(self, contents: str): processor_pattern = re.compile( - f"{self.REPLACEMENT_TOKEN}(.*?){self.ARGS_TOKEN}(.*){self.REPLACEMENT_TOKEN}" + f"{self.REPLACEMENT_TOKEN}(.*?){self.ARGS_TOKEN}(.*?){self.REPLACEMENT_TOKEN}" ) matches = list(processor_pattern.finditer(contents)) @@ -102,8 +102,8 @@ class Processor: with open(src_path, "w") as f: f.write(contents) - def process_dir(self, dir_path: str): - for filename in glob(dir_path + "/**/*.py", recursive=True): + def process_dir(self, dir_path: str, *, file_ext: str = ".py"): + for filename in glob(dir_path + f"/**/*{file_ext}", recursive=True): try: self.process_file(filename) except Exception as e: @@ -117,26 +117,36 @@ def parse_schema(path): def parse_args(args=None): - parser = ArgumentParser( + parser = argparse.ArgumentParser( add_help=True, - description=""" - Processes generator output files in a custom way, saves results inplace. - - Replacement token: '%(repl_token)s'. - Args separator token: '%(args_token)s'. - Replaces the following patterns in python files: - '%(repl_token)sREPLACER%(args_token)sARG1%(args_token)sARG2...%(repl_token)s' - -> - REPLACER(ARG1, ARG2, ...) value - + formatter_class=argparse.RawTextHelpFormatter, + description="""\ +Processes generator output files in a custom way, saves results inplace. + +Replacement token: '%(repl_token)s'. +Arg separator token: '%(args_token)s'. +Replaces the following patterns in files: + '%(repl_token)sREPLACER%(args_token)sARG1%(args_token)sARG2...%(repl_token)s' + -> + REPLACER(ARG1, ARG2, ...) value + +Available REPLACERs: + %(replacers)s """ % { - "repl_token": Processor.REPLACEMENT_TOKEN, - "args_token": Processor.ARGS_TOKEN, + "repl_token": Replacer.REPLACEMENT_TOKEN, + "args_token": Replacer.ARGS_TOKEN, + "replacers": "\n ".join(Replacer.allowed_actions), }, ) - parser.add_argument("--schema", required=True) - parser.add_argument("--input-path", required=True) + parser.add_argument("--schema", required=True, help="Path to server schema yaml") + parser.add_argument("--input-path", required=True, help="Path to target file or directory") + parser.add_argument( + "--file-ext", + default=".py", + help="If working on a directory, look for " + "files with the specified extension (default: %(default)s)", + ) return parser.parse_args(args) @@ -145,10 +155,10 @@ def main(args=None): args = parse_args(args) schema = parse_schema(args.schema) - processor = Processor(schema=schema) + processor = Replacer(schema=schema) if osp.isdir(args.input_path): - processor.process_dir(args.input_path) + processor.process_dir(args.input_path, file_ext=args.file_ext) elif osp.isfile(args.input_path): processor.process_file(args.input_path) diff --git a/cvat-sdk/gen/templates/README.md.template b/cvat-sdk/gen/templates/README.md.template new file mode 100644 index 000000000..a2d85c237 --- /dev/null +++ b/cvat-sdk/gen/templates/README.md.template @@ -0,0 +1,26 @@ +# SDK for [Computer Vision Annotation Tool (CVAT)](https://github.com/cvat-ai/cvat) + +This package provides a Python client library for CVAT server. It can be useful for +workflow automation and writing custom CVAT server clients. + +The SDK API includes 2 layers: +- Server API wrappers (`ApiClient`). Located in at `cvat_sdk.api_client` +- High-level tools (`Core`). Located at `cvat_sdk.core` + +Package documentation is available [here](https://opencv.github.io/cvat/docs/api_sdk/sdk). + +## Installation & Usage + +To install a prebuilt package, run the following command in the terminal: + +```bash +pip install cvat-sdk +``` + +To install from the local directory, follow [the developer guide](https://opencv.github.io/cvat/docs/api_sdk/sdk/developer_guide). + +After installation you can import the package: + +```python +import cvat_sdk +``` diff --git a/cvat-sdk/gen/templates/openapi-generator/README.mustache b/cvat-sdk/gen/templates/openapi-generator/README.mustache index d48a0ac0c..95480268f 100644 --- a/cvat-sdk/gen/templates/openapi-generator/README.mustache +++ b/cvat-sdk/gen/templates/openapi-generator/README.mustache @@ -23,7 +23,7 @@ To install a prebuilt package, run the following command in the terminal: pip install cvat-sdk ``` -To install from the local directory, follow [the developer guide](https://opencv.github.io/cvat/docs/integration/sdk/developer_guide). +To install from the local directory, follow [the developer guide](https://opencv.github.io/cvat/docs/api_sdk/sdk/developer_guide). After installation you can import the package: diff --git a/cvat-sdk/gen/templates/openapi-generator/README_common.mustache b/cvat-sdk/gen/templates/openapi-generator/README_common.mustache index 6515c8b62..a14bbfe36 100644 --- a/cvat-sdk/gen/templates/openapi-generator/README_common.mustache +++ b/cvat-sdk/gen/templates/openapi-generator/README_common.mustache @@ -155,16 +155,16 @@ with make_client(host="{{{basePath}}}") as client: task.remove() ``` -## Documentation for API Endpoints +## Available API Endpoints -All URIs are relative to *{{basePath}}* +All URIs are relative to _{{basePath}}_ Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- -{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | **{{operationId}}** | **{{httpMethod}}** {{path}} | {{summary}} +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}_{{classname}}_ | [**{{>operation_name}}**](apis/{{classname}}#{{>operation_name}}) | **{{httpMethod}}** {{path}} | {{summary}} {{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} -## Documentation For Models +## Available Models {{#models}}{{#model}} - {{{classname}}} {{/model}}{{/models}} diff --git a/cvat-sdk/gen/templates/openapi-generator/api_doc.mustache b/cvat-sdk/gen/templates/openapi-generator/api_doc.mustache new file mode 100644 index 000000000..ed8d97d51 --- /dev/null +++ b/cvat-sdk/gen/templates/openapi-generator/api_doc.mustache @@ -0,0 +1,59 @@ +# {{classname}} + +{{#description}}{{.}} +{{/description}} + +All URIs are relative to _{{basePath}}_ + +Method | HTTP request | Description +------------- | ------------- | ------------- +{{#operations}}{{#operation}}[**{{>operation_name}}**]({{classname}}#{{>operation_name}}) | **{{httpMethod}}** {{path}} | {{summary}} +{{/operation}}{{/operations}} + +{{#operations}} +{{#operation}} +## **{{>operation_name}}** +> {{#returnType}}{{{.}}} {{/returnType}}{{>operation_name}}({{#requiredParams}}{{^defaultValue}}{{paramName}}{{^-last}}, {{/-last}}{{/defaultValue}}{{/requiredParams}}) + +{{{summary}}}{{#notes}} + +{{{.}}}{{/notes}} + +### Example +{{> api_doc_example }} + +### Parameters +{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{#allParams}}{{#-last}} +Name | Type | Description | Notes +------------- | ------------- | ------------- | -------------{{/-last}}{{/allParams}} +{{#requiredParams}}{{^defaultValue}} **{{paramName}}** | {{^baseType}}**{{dataType}}**{{/baseType}}{{#baseType}}[**{{dataType}}**](../models/{{baseType}}){{/baseType}}| {{description}} | +{{/defaultValue}}{{/requiredParams}}{{#requiredParams}}{{#defaultValue}} **{{paramName}}** | {{^baseType}}**{{dataType}}**{{/baseType}}{{#baseType}}[**{{dataType}}**](../models/{{baseType}}){{/baseType}}| {{description}} | defaults to {{{.}}} +{{/defaultValue}}{{/requiredParams}}{{#optionalParams}} **{{paramName}}** | {{^baseType}}**{{dataType}}**{{/baseType}}{{#baseType}}[**{{dataType}}**](../models/{{baseType}}){{/baseType}}| {{description}} | [optional]{{#defaultValue}} if omitted the server will use the default value of {{{.}}}{{/defaultValue}} +{{/optionalParams}} + +### Return type + +{{#returnType}}{{#returnTypeIsPrimitive}}**{{{returnType}}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}[**{{{returnType}}}**](../models/{{returnBaseType}}){{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}None (empty response body){{/returnType}} + +### Authorization + +{{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}{{{name}}}{{^-last}}, {{/-last}}{{/authMethods}} + +### HTTP request headers + + - **Content-Type**: {{#consumes}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} + - **Accept**: {{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{^produces}}Not defined{{/produces}} + +{{#responses.0}} + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +{{#responses}} +**{{code}}** | {{message}} | {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}} | +{{/responses}} +{{/responses.0}} + +{{/operation}} +{{/operations}} diff --git a/cvat-sdk/gen/templates/openapi-generator/api_doc_example.mustache b/cvat-sdk/gen/templates/openapi-generator/api_doc_example.mustache new file mode 100644 index 000000000..d671039b3 --- /dev/null +++ b/cvat-sdk/gen/templates/openapi-generator/api_doc_example.mustache @@ -0,0 +1,44 @@ +```python +import time +from {{{packageName}}} import Configuration, ApiClient, exceptions +{{#imports}} +{{.}} +{{/imports}} +from pprint import pprint + +# Set up an API client +# Read Configuration class docs for more info about parameters and authentication methods +configuration = Configuration( + host = "{{{basePath}}}",{{#hasAuthMethods}} +{{#authMethods}} +{{#isBasic}} +{{#isBasicBasic}} + username = 'YOUR_USERNAME', + password = 'YOUR_PASSWORD', +{{/isBasicBasic}} +{{/isBasic}} +{{/authMethods}} +{{/hasAuthMethods}} +) + +with ApiClient(configuration) as api_client: +{{#requiredParams}} +{{^defaultValue}} + {{paramName}} = {{{example}}} # {{{dataType}}} | {{{description}}} +{{/defaultValue}} +{{/requiredParams}} +{{#optionalParams}} + {{paramName}} = {{{example}}} # {{{dataType}}} | {{{description}}}{{^required}} (optional){{/required}}{{#defaultValue}} if omitted the server will use the default value of {{{.}}}{{/defaultValue}} +{{/optionalParams}} + + try: + {{#returnType}}(data, response) = {{/returnType}}%%%make_api_name!!!{{classname}}%%%.{{{operationId}}}({{#requiredParams}} + {{^defaultValue}}{{paramName}},{{/defaultValue}}{{/requiredParams}}{{#optionalParams}} + {{paramName}}={{paramName}},{{#-last}} + {{/-last}}{{/optionalParams}}) +{{#returnType}} + pprint(data) +{{/returnType}} + except exceptions.ApiException as e: + print("Exception when calling {{classname}}.{{operationId}}: %s\n" % e) +``` diff --git a/cvat-sdk/gen/templates/openapi-generator/model_doc.mustache b/cvat-sdk/gen/templates/openapi-generator/model_doc.mustache new file mode 100644 index 000000000..94d13c14e --- /dev/null +++ b/cvat-sdk/gen/templates/openapi-generator/model_doc.mustache @@ -0,0 +1,32 @@ +{{#models}}{{#model}}# {{classname}} + +{{#description}}{{&description}} +{{/description}} + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +{{#isEnum}} +**value** | {{^arrayModelType}}**{{dataType}}**{{/arrayModelType}} | {{description}} | {{#defaultValue}}{{#hasRequired}} if omitted the server will use the default value of {{/hasRequired}}{{^hasRequired}}defaults to {{/hasRequired}}{{{.}}}{{/defaultValue}}{{#allowableValues}}{{#defaultValue}}, {{/defaultValue}} must be one of [{{#enumVars}}{{{value}}}, {{/enumVars}}]{{/allowableValues}} +{{/isEnum}} +{{#isAlias}} +**value** | {{^arrayModelType}}**{{dataType}}**{{/arrayModelType}} | {{description}} | {{#defaultValue}}{{#hasRequired}} if omitted the server will use the default value of {{/hasRequired}}{{^hasRequired}}defaults to {{/hasRequired}}{{{.}}}{{/defaultValue}} +{{/isAlias}} +{{#isArray}} +**value** | {{^arrayModelType}}**{{dataType}}**{{/arrayModelType}}{{#arrayModelType}}[**{{dataType}}**]({{arrayModelType}}){{/arrayModelType}} | {{description}} | {{#defaultValue}}{{#hasRequired}} if omitted the server will use the default value of {{/hasRequired}}{{^hasRequired}}defaults to {{/hasRequired}}{{{.}}}{{/defaultValue}} +{{/isArray}} +{{#requiredVars}} +{{^defaultValue}} +**{{name}}** | {{^complexType}}**{{dataType}}**{{/complexType}}{{#complexType}}[**{{dataType}}**]({{complexType}}){{/complexType}} | {{description}} | {{#isReadOnly}}[readonly] {{/isReadOnly}} +{{/defaultValue}} +{{/requiredVars}} +{{#requiredVars}} +{{#defaultValue}} +**{{name}}** | {{^complexType}}**{{dataType}}**{{/complexType}}{{#complexType}}[**{{dataType}}**]({{complexType}}){{/complexType}} | {{description}} | {{^required}}[optional] {{/required}}{{#isReadOnly}}[readonly] {{/isReadOnly}}{{#defaultValue}}defaults to {{{.}}}{{/defaultValue}} +{{/defaultValue}} +{{/requiredVars}} +{{#optionalVars}} +**{{name}}** | {{^complexType}}**{{dataType}}**{{/complexType}}{{#complexType}}[**{{dataType}}**]({{complexType}}){{/complexType}} | {{description}} | [optional] {{#isReadOnly}}[readonly] {{/isReadOnly}}{{#defaultValue}} if omitted the server will use the default value of {{{.}}}{{/defaultValue}} +{{/optionalVars}} + +{{/model}}{{/models}} diff --git a/cvat/apps/engine/schema.py b/cvat/apps/engine/schema.py index c228d67f3..8ebd21def 100644 --- a/cvat/apps/engine/schema.py +++ b/cvat/apps/engine/schema.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: MIT +import textwrap from typing import Type from rest_framework import serializers from drf_spectacular.extensions import OpenApiSerializerExtension @@ -78,6 +79,8 @@ class WriteOnceSerializerExtension(OpenApiSerializerExtension): """ Enables support for cvat.apps.engine.serializers.WriteOnceMixin in drf-spectacular. Doesn't block other extensions on the target serializer. + + Removes the WriteOnceMixin class docstring from derived class descriptions. """ match_subclasses = True @@ -94,13 +97,20 @@ class WriteOnceSerializerExtension(OpenApiSerializerExtension): return False def map_serializer(self, auto_schema, direction): - return auto_schema._map_serializer( + from cvat.apps.engine.serializers import WriteOnceMixin + + schema = auto_schema._map_serializer( _copy_serializer(self.target, context={ 'view': auto_schema.view, self._PROCESSED_INDICATOR_NAME: True }), direction, bypass_extensions=False) + if schema.get('description') == textwrap.dedent(WriteOnceMixin.__doc__).strip(): + del schema['description'] + + return schema + class OpenApiTypeProxySerializerExtension(PolymorphicProxySerializerExtension): """ Provides support for OpenApiTypes in the PolymorphicProxySerializer list diff --git a/site/content/en/docs/api_sdk/sdk/_index.md b/site/content/en/docs/api_sdk/sdk/_index.md index 51a30d5b1..75d9eae01 100644 --- a/site/content/en/docs/api_sdk/sdk/_index.md +++ b/site/content/en/docs/api_sdk/sdk/_index.md @@ -11,8 +11,8 @@ SDK is a Python library. It provides you access to Python function and objects, simplify server interaction and provide additional functionality like data validation. SDK API includes 2 layers: -- Low-level API with REST API wrappers. Located in at `cvat_sdk.api_client`. [Read more](/api_sdk/sdk/lowlevel-api) -- High-level API. Located at `cvat_sdk.core`. [Read more](/api_sdk/sdk/highlevel-api) +- Low-level API with REST API wrappers. Located in at `cvat_sdk.api_client`. [Read more](../sdk/lowlevel-api) +- High-level API. Located at `cvat_sdk.core`. [Read more](../sdk/highlevel-api) Roughly, low-level API provides single-request operations, while the high-level one allows you to use composite, multi-request operations and have local counterparts for server objects. diff --git a/site/content/en/docs/api_sdk/sdk/developer-guide.md b/site/content/en/docs/api_sdk/sdk/developer-guide.md index f9287cf32..0cf3f2fc3 100644 --- a/site/content/en/docs/api_sdk/sdk/developer-guide.md +++ b/site/content/en/docs/api_sdk/sdk/developer-guide.md @@ -7,7 +7,7 @@ description: '' ## Overview -This package contains manually written and generated files. We store only sources in +This package contains manually written and autogenerated files. We store only sources in the repository. To get the full package, one need to generate missing package files. ## Package file layout @@ -23,7 +23,7 @@ the repository. To get the full package, one need to generate missing package fi If you have a local custom version of the server, run the following command in the terminal. You need to be able to execute django server. Server installation instructions are available -[here](/contributing/development-environment). +[here](/docs/contributing/development-environment). ```bash mkdir -p cvat-sdk/schema/ && python manage.py spectacular --file cvat-sdk/schema/schema.yml ``` @@ -41,6 +41,10 @@ you can also get schema from `/api/docs`: ![Download server schema button image](/images/download_server_schema.png) +The official server schema for `app.cvat.ai` is available [here](https://app.cvat.ai/api/docs/). + +You can read more about server schema [here](/docs/api_sdk/api#api-schema). + 2. Install generator dependencies: ```bash pip install -r gen/requirements.txt @@ -78,3 +82,72 @@ To execute, run: ```bash pytest tests/python/rest_api tests/python/sdk ``` + +## SDK API design decisions + +The generated `ApiClient` code is modified from what `openapi-generator` does by default. +Changes are mostly focused on better user experience - including better +usage patterns and simpler/faster ways to achieve results. + +### Modifications + +- Added Python type annotations for return types and class members. + This change required us to implement a custom post-processing script, + which converts generated types into correct type annotations. The types + generated by default are supposed to work with the API implementation + (parameter validation and parsing), but they are not applicable as + type annotations (they have incorrect syntax). Custom post-processing + allowed us to make these types correct type annotations. + Other possible solutions: + - There is the `python-experimental` API generator, which may solve + some issues, but it is unstable and requires python 3.9. Our API + works with 3.7, which is the lowest supported version now. + - Custom templates - partially works, but only in limited cases + (model fields). It's very hard to maintain the template code and + logic for this. Only `if` checks and `for` loops are available in + mustache templates, which is not enough for annotation generation. + +- Separate APIs are embedded into the general `APIClient` class. + Now we have: + ```python + with ApiClient(config) as api_client: + result1 = api_client.foo_api.operation1() + result2 = api_client.bar_api.operation2() + ``` + + This showed to be more convenient than the default: + ```python + with ApiClient(config) as api_client: + foo_api = FooApi(api_client) + result1 = foo_api.operation1() + result2 = foo_api.operation2() + + bar_api = BarApi(api_client) + result3 = bar_api.operation3() + result4 = bar_api.operation4() + ``` + + This also required custom post-processing. Operation Ids are + [supposed to be unique](https://swagger.io/specification/#operation-object) + in the OpenAPI / Swagger specification. Therefore, we can't generate such + schema on the server, nor we can't expect it to be supported in the + API generator. + +- Operations have IDs like `/_`. + This also showed to be more readable and more natural than DRF-spectacular's + default `/_`. + +- Server operations have different types for input and output values. + While it can be expected that an endopint with POST/PUT methods available + (like `create` or `partial_update`) has the same type for input and output + (because it looks natural), it also leads to the situation, in which there + are lots of read-/write-only fields, and it becomes hard for understanding. + This clear type separation is supposed to make it simpler for users. + +- Added cookie management in the `ApiClient` class. + +- Added interface classes for models to simplify class member usage and lookup. + +- Dicts can be passed into API methods and model constructors instead of models. + They are automatically parsed as models. In the original implementation, the user + is required to pass a `Configuration` object each time, which is clumsy and adds little sense. diff --git a/site/content/en/docs/api_sdk/sdk/lowlevel-api.md b/site/content/en/docs/api_sdk/sdk/lowlevel-api.md index 20ed4c52e..49be93022 100644 --- a/site/content/en/docs/api_sdk/sdk/lowlevel-api.md +++ b/site/content/en/docs/api_sdk/sdk/lowlevel-api.md @@ -7,7 +7,7 @@ description: '' ## Overview -Low-level API is useful if you need to work directly with REST API, but want +The low-level API is useful if you need to work directly with REST API, but want to have data validation and syntax assistance from your code editor. The code on this layer is autogenerated. @@ -103,146 +103,119 @@ with ApiClient(configuration) as api_client: assert task.size == 4 ``` -## Available API Endpoints +## ApiClient and configuration -All URIs are relative to _`http://localhost`_ +The starting point in the low-level API is the `cvat_sdk.api_client.ApiClient` class. +It encapsulates session and connection logic, manages headers and cookies, +and provides access to various APIs. -APIs can be instanted directly like this: +To create an instance of `ApiClient`, you need to set up a `cvat_sdk.api_client.Configuration` +object and pass it to the `ApiClient` class constructor. Additional connection-specific +options, such as extra headers and cookies can be specified in the class constructor. +`ApiClient` implements the context manager protocol. Typically, you create `ApiClient` this way: + +```python +from cvat_sdk.api_client import ApiClient, Configuration + +configuration = Configuration(host="http://localhost") +with ApiClient(configuration) as api_client: + ... +``` + +After creating an `ApiClient` instance, you can send requests to various server endpoints +via `*_api` member properties and directly, using the `rest_client` member. +[Read more](#api-wrappers) about API wrappers below. + +Typically, the first thing you do with `ApiClient` is log in. +[Read more](#authentication) about authentication options below. + +## Authentication + +CVAT supports 2 authentication options: +- basic auth, with your username and password +- token auth, with your API key + +Token auth requires a token, which can be obtained after performing the basic auth. + +The low-level API supports 2 ways of authentication. +You can specify authentication parameters in the `Configuration` object: + +```python +configuration = Configuration( + username='YOUR_USERNAME', + password='YOUR_PASSWORD', +) +``` + +```python +configuration = Configuration( + api_key={ + "sessionAuth": "", + "csrfAuth": "", + "tokenAuth": "Token ", + } +) +``` + +You can perform a regular login using the `auth_api` member of `ApiClient` and +set the `Authorization` header using the `Token` prefix. This way, you'll be able to +obtain API tokens, which can be reused in the future to avoid typing your credentials. + +```python +from cvat_sdk.api_client import models + +(auth, _) = api_client.auth_api.create_login( + models.LoginRequest(username=credentials[0], password=credentials[1]) +) + +assert "sessionid" in api_client.cookies +assert "csrftoken" in api_client.cookies +api_client.set_default_header("Authorization", "Token " + auth.key) +``` + +## API wrappers + +API endpoints are grouped by tags into separate classes in the `cvat_sdk.api_client.apis` package. + +APIs can be accessed as `ApiClient` object members: + +```python +api_client.auth_api.(...) +api_client.tasks_api.(...) +``` + +And APIs can be instantiated directly like this: ```python from cvat_sdk.api_client import ApiClient, apis api_client = ApiClient(...) + auth_api = apis.AuthApi(api_client) auth_api.(...) -``` -Or they can be accessed as `ApiClient` object members: +tasks_api = apis.TasksApi(api_client) +tasks_api.(...) ``` -from cvat_sdk.api_client import ApiClient -api_client = ApiClient(...) -api_client.auth_api.(...) -``` +For each operation, the API wrapper class has a corresponding `_endpoint` member. +This member represents the endpoint as a first-class object, which provides metainformation +about the endpoint, such as the relative URL of the endpoint, parameter names, +types and their placement in the request. It also allows to pass the operation to other +functions and invoke it from there. + +For a typical server entity like `Task`, `Project`, `Job` etc., the `*Api` classes provide methods +that reflect Create-Read-Update-Delete (CRUD) operations: `create`, `retrieve`, `list`, `update`, +`partial_update`, `delete`. The set of available operations depends on the entity type. + +You can find the list of the available APIs and their documentation [here](../reference/apis/). - - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -_AuthApi_ | **auth_create_login** | **POST** /api/auth/login | -_AuthApi_ | **auth_create_logout** | **POST** /api/auth/logout | -_AuthApi_ | **auth_create_password_change** | **POST** /api/auth/password/change | -_AuthApi_ | **auth_create_password_reset** | **POST** /api/auth/password/reset | -_AuthApi_ | **auth_create_password_reset_confirm** | **POST** /api/auth/password/reset/confirm | -_AuthApi_ | **auth_create_register** | **POST** /api/auth/register | -_AuthApi_ | **auth_create_signing** | **POST** /api/auth/signing | This method signs URL for access to the server -_CloudstoragesApi_ | **cloudstorages_create** | **POST** /api/cloudstorages | Method creates a cloud storage with a specified characteristics -_CloudstoragesApi_ | **cloudstorages_destroy** | **DELETE** /api/cloudstorages/{id} | Method deletes a specific cloud storage -_CloudstoragesApi_ | **cloudstorages_list** | **GET** /api/cloudstorages | Returns a paginated list of storages according to query parameters -_CloudstoragesApi_ | **cloudstorages_partial_update** | **PATCH** /api/cloudstorages/{id} | Methods does a partial update of chosen fields in a cloud storage instance -_CloudstoragesApi_ | **cloudstorages_retrieve** | **GET** /api/cloudstorages/{id} | Method returns details of a specific cloud storage -_CloudstoragesApi_ | **cloudstorages_retrieve_actions** | **GET** /api/cloudstorages/{id}/actions | Method returns allowed actions for the cloud storage -_CloudstoragesApi_ | **cloudstorages_retrieve_content** | **GET** /api/cloudstorages/{id}/content | Method returns a manifest content -_CloudstoragesApi_ | **cloudstorages_retrieve_preview** | **GET** /api/cloudstorages/{id}/preview | Method returns a preview image from a cloud storage -_CloudstoragesApi_ | **cloudstorages_retrieve_status** | **GET** /api/cloudstorages/{id}/status | Method returns a cloud storage status -_CommentsApi_ | **comments_create** | **POST** /api/comments | Method creates a comment -_CommentsApi_ | **comments_destroy** | **DELETE** /api/comments/{id} | Method deletes a comment -_CommentsApi_ | **comments_list** | **GET** /api/comments | Method returns a paginated list of comments according to query parameters -_CommentsApi_ | **comments_partial_update** | **PATCH** /api/comments/{id} | Methods does a partial update of chosen fields in a comment -_CommentsApi_ | **comments_retrieve** | **GET** /api/comments/{id} | Method returns details of a comment -_InvitationsApi_ | **invitations_create** | **POST** /api/invitations | Method creates an invitation -_InvitationsApi_ | **invitations_destroy** | **DELETE** /api/invitations/{key} | Method deletes an invitation -_InvitationsApi_ | **invitations_list** | **GET** /api/invitations | Method returns a paginated list of invitations according to query parameters -_InvitationsApi_ | **invitations_partial_update** | **PATCH** /api/invitations/{key} | Methods does a partial update of chosen fields in an invitation -_InvitationsApi_ | **invitations_retrieve** | **GET** /api/invitations/{key} | Method returns details of an invitation -_IssuesApi_ | **issues_create** | **POST** /api/issues | Method creates an issue -_IssuesApi_ | **issues_destroy** | **DELETE** /api/issues/{id} | Method deletes an issue -_IssuesApi_ | **issues_list** | **GET** /api/issues | Method returns a paginated list of issues according to query parameters -_IssuesApi_ | **issues_list_comments** | **GET** /api/issues/{id}/comments | The action returns all comments of a specific issue -_IssuesApi_ | **issues_partial_update** | **PATCH** /api/issues/{id} | Methods does a partial update of chosen fields in an issue -_IssuesApi_ | **issues_retrieve** | **GET** /api/issues/{id} | Method returns details of an issue -_JobsApi_ | **jobs_create_annotations** | **POST** /api/jobs/{id}/annotations/ | Method allows to upload job annotations -_JobsApi_ | **jobs_destroy_annotations** | **DELETE** /api/jobs/{id}/annotations/ | Method deletes all annotations for a specific job -_JobsApi_ | **jobs_list** | **GET** /api/jobs | Method returns a paginated list of jobs according to query parameters -_JobsApi_ | **jobs_list_commits** | **GET** /api/jobs/{id}/commits | The action returns the list of tracked changes for the job -_JobsApi_ | **jobs_list_issues** | **GET** /api/jobs/{id}/issues | Method returns list of issues for the job -_JobsApi_ | **jobs_partial_update** | **PATCH** /api/jobs/{id} | Methods does a partial update of chosen fields in a job -_JobsApi_ | **jobs_partial_update_annotations** | **PATCH** /api/jobs/{id}/annotations/ | Method performs a partial update of annotations in a specific job -_JobsApi_ | **jobs_partial_update_annotations_file** | **PATCH** /api/jobs/{id}/annotations/{file_id} | Allows to upload an annotation file chunk. Implements TUS file uploading protocol. -_JobsApi_ | **jobs_retrieve** | **GET** /api/jobs/{id} | Method returns details of a job -_JobsApi_ | **jobs_retrieve_annotations** | **GET** /api/jobs/{id}/annotations/ | Method returns annotations for a specific job as a JSON document. If format is specified a zip archive is returned. -_JobsApi_ | **jobs_retrieve_data** | **GET** /api/jobs/{id}/data | Method returns data for a specific job -_JobsApi_ | **jobs_retrieve_data_meta** | **GET** /api/jobs/{id}/data/meta | Method provides a meta information about media files which are related with the job -_JobsApi_ | **jobs_retrieve_dataset** | **GET** /api/jobs/{id}/dataset | Export job as a dataset in a specific format -_JobsApi_ | **jobs_update_annotations** | **PUT** /api/jobs/{id}/annotations/ | Method performs an update of all annotations in a specific job -_LambdaApi_ | **lambda_create_functions** | **POST** /api/lambda/functions/{func_id} | -_LambdaApi_ | **lambda_create_requests** | **POST** /api/lambda/requests | Method calls the function -_LambdaApi_ | **lambda_list_functions** | **GET** /api/lambda/functions | Method returns a list of functions -_LambdaApi_ | **lambda_list_requests** | **GET** /api/lambda/requests | Method returns a list of requests -_LambdaApi_ | **lambda_retrieve_functions** | **GET** /api/lambda/functions/{func_id} | Method returns the information about the function -_LambdaApi_ | **lambda_retrieve_requests** | **GET** /api/lambda/requests/{id} | Method returns the status of the request -_MembershipsApi_ | **memberships_destroy** | **DELETE** /api/memberships/{id} | Method deletes a membership -_MembershipsApi_ | **memberships_list** | **GET** /api/memberships | Method returns a paginated list of memberships according to query parameters -_MembershipsApi_ | **memberships_partial_update** | **PATCH** /api/memberships/{id} | Methods does a partial update of chosen fields in a membership -_MembershipsApi_ | **memberships_retrieve** | **GET** /api/memberships/{id} | Method returns details of a membership -_OrganizationsApi_ | **organizations_create** | **POST** /api/organizations | Method creates an organization -_OrganizationsApi_ | **organizations_destroy** | **DELETE** /api/organizations/{id} | Method deletes an organization -_OrganizationsApi_ | **organizations_list** | **GET** /api/organizations | Method returns a paginated list of organizatins according to query parameters -_OrganizationsApi_ | **organizations_partial_update** | **PATCH** /api/organizations/{id} | Methods does a partial update of chosen fields in an organization -_OrganizationsApi_ | **organizations_retrieve** | **GET** /api/organizations/{id} | Method returns details of an organization -_ProjectsApi_ | **projects_create** | **POST** /api/projects | Method creates a new project -_ProjectsApi_ | **projects_create_backup** | **POST** /api/projects/backup/ | Methods create a project from a backup -_ProjectsApi_ | **projects_create_dataset** | **POST** /api/projects/{id}/dataset/ | Import dataset in specific format as a project -_ProjectsApi_ | **projects_destroy** | **DELETE** /api/projects/{id} | Method deletes a specific project -_ProjectsApi_ | **projects_list** | **GET** /api/projects | Returns a paginated list of projects according to query parameters (12 projects per page) -_ProjectsApi_ | **projects_list_tasks** | **GET** /api/projects/{id}/tasks | Method returns information of the tasks of the project with the selected id -_ProjectsApi_ | **projects_partial_update** | **PATCH** /api/projects/{id} | Methods does a partial update of chosen fields in a project -_ProjectsApi_ | **projects_partial_update_backup_file** | **PATCH** /api/projects/backup/{file_id} | Allows to upload a file chunk. Implements TUS file uploading protocol -_ProjectsApi_ | **projects_partial_update_dataset_file** | **PATCH** /api/projects/{id}/dataset/{file_id} | Allows to upload a file chunk. Implements TUS file uploading protocol. -_ProjectsApi_ | **projects_retrieve** | **GET** /api/projects/{id} | Method returns details of a specific project -_ProjectsApi_ | **projects_retrieve_annotations** | **GET** /api/projects/{id}/annotations | Method allows to download project annotations -_ProjectsApi_ | **projects_retrieve_backup** | **GET** /api/projects/{id}/backup | Methods creates a backup copy of a project -_ProjectsApi_ | **projects_retrieve_dataset** | **GET** /api/projects/{id}/dataset/ | Export project as a dataset in a specific format -_RestrictionsApi_ | **restrictions_retrieve_terms_of_use** | **GET** /api/restrictions/terms-of-use | Method provides CVAT terms of use -_RestrictionsApi_ | **restrictions_retrieve_user_agreements** | **GET** /api/restrictions/user-agreements | Method provides user agreements that the user must accept to register -_SchemaApi_ | **schema_retrieve** | **GET** /api/schema/ | -_ServerApi_ | **server_create_exception** | **POST** /api/server/exception | Method saves an exception from a client on the server -_ServerApi_ | **server_create_logs** | **POST** /api/server/logs | Method saves logs from a client on the server -_ServerApi_ | **server_list_share** | **GET** /api/server/share | Returns all files and folders that are on the server along specified path -_ServerApi_ | **server_retrieve_about** | **GET** /api/server/about | Method provides basic CVAT information -_ServerApi_ | **server_retrieve_annotation_formats** | **GET** /api/server/annotation/formats | Method provides the list of supported annotations formats -_ServerApi_ | **server_retrieve_plugins** | **GET** /api/server/plugins | Method provides allowed plugins -_TasksApi_ | **jobs_partial_update_data_meta** | **PATCH** /api/jobs/{id}/data/meta | Method provides a meta information about media files which are related with the job -_TasksApi_ | **tasks_create** | **POST** /api/tasks | Method creates a new task in a database without any attached images and videos -_TasksApi_ | **tasks_create_annotations** | **POST** /api/tasks/{id}/annotations/ | Method allows to upload task annotations from a local file or a cloud storage -_TasksApi_ | **tasks_create_backup** | **POST** /api/tasks/backup/ | Method recreates a task from an attached task backup file -_TasksApi_ | **tasks_create_data** | **POST** /api/tasks/{id}/data/ | Method permanently attaches images or video to a task. Supports tus uploads, see more -_TasksApi_ | **tasks_destroy** | **DELETE** /api/tasks/{id} | Method deletes a specific task, all attached jobs, annotations, and data -_TasksApi_ | **tasks_destroy_annotations** | **DELETE** /api/tasks/{id}/annotations/ | Method deletes all annotations for a specific task -_TasksApi_ | **tasks_list** | **GET** /api/tasks | Returns a paginated list of tasks according to query parameters (10 tasks per page) -_TasksApi_ | **tasks_list_jobs** | **GET** /api/tasks/{id}/jobs | Method returns a list of jobs for a specific task -_TasksApi_ | **tasks_partial_update** | **PATCH** /api/tasks/{id} | Methods does a partial update of chosen fields in a task -_TasksApi_ | **tasks_partial_update_annotations** | **PATCH** /api/tasks/{id}/annotations/ | Method performs a partial update of annotations in a specific task -_TasksApi_ | **tasks_partial_update_annotations_file** | **PATCH** /api/tasks/{id}/annotations/{file_id} | Allows to upload an annotation file chunk. Implements TUS file uploading protocol. -_TasksApi_ | **tasks_partial_update_backup_file** | **PATCH** /api/tasks/backup/{file_id} | Allows to upload a file chunk. Implements TUS file uploading protocol. -_TasksApi_ | **tasks_partial_update_data_file** | **PATCH** /api/tasks/{id}/data/{file_id} | Allows to upload a file chunk. Implements TUS file uploading protocol. -_TasksApi_ | **tasks_partial_update_data_meta** | **PATCH** /api/tasks/{id}/data/meta | Method provides a meta information about media files which are related with _he_task -_TasksApi_ | **tasks_retrieve** | **GET** /api/tasks/{id} | Method returns details of a specific task -_TasksApi_ | **tasks_retrieve_annotations** | **GET** /api/tasks/{id}/annotations/ | Method allows to download task annotations -_TasksApi_ | **tasks_retrieve_backup** | **GET** /api/tasks/{id}/backup | Method backup a specified task -_TasksApi_ | **tasks_retrieve_data** | **GET** /api/tasks/{id}/data/ | Method returns data for a specific task -_TasksApi_ | **tasks_retrieve_data_meta** | **GET** /api/tasks/{id}/data/meta | Method provides a meta information about media files which are related with the task -_TasksApi_ | **tasks_retrieve_dataset** | **GET** /api/tasks/{id}/dataset | Export task as a dataset in a specific format -_TasksApi_ | **tasks_retrieve_status** | **GET** /api/tasks/{id}/status | When task is being created the method returns information about a status of the creation process -_TasksApi_ | **tasks_update_annotations** | **PUT** /api/tasks/{id}/annotations/ | Method allows to upload task annotations -_UsersApi_ | **users_destroy** | **DELETE** /api/users/{id} | Method deletes a specific user from the server -_UsersApi_ | **users_list** | **GET** /api/users | Method provides a paginated list of users registered on the server -_UsersApi_ | **users_partial_update** | **PATCH** /api/users/{id} | Method updates chosen fields of a user -_UsersApi_ | **users_retrieve** | **GET** /api/users/{id} | Method provides information of a specific user -_UsersApi_ | **users_retrieve_self** | **GET** /api/users/self | Method returns an instance of a user who is currently authorized - - -## Available Models +## Models + +Requests and responses can include data. It can be represented as plain Python +data structures and model classes (or models). In CVAT API, model for requests and responses +are separated: the request models have the `Request` suffix in the name, while the response +models have no suffix. Models can be found in the `cvat_sdk.api_client.models` package. Models can be instantiated like this: @@ -252,135 +225,141 @@ from cvat_sdk.api_client import models user_model = models.User(...) ``` -- About -- AnnotationFileRequest -- AnnotationsRead -- Attribute -- AttributeRequest -- AttributeVal -- AttributeValRequest -- BackupWriteRequest -- BasicUser -- BasicUserRequest -- ChunkType -- CloudStorageRead -- CloudStorageWriteRequest -- CommentRead -- CommentReadOwner -- CommentWriteRequest -- CredentialsTypeEnum -- DataMetaRead -- DataRequest -- DatasetFileRequest -- DatasetFormat -- DatasetFormats -- DatasetWriteRequest -- Exception -- ExceptionRequest -- FileInfo -- FileInfoTypeEnum -- FrameMeta -- InputTypeEnum -- InvitationRead -- InvitationWrite -- InvitationWriteRequest -- IssueRead -- IssueWriteRequest -- JobAnnotationsUpdateRequest -- JobCommit -- JobRead -- JobStage -- JobStatus -- Label -- LabeledData -- LabeledDataRequest -- LabeledImage -- LabeledImageRequest -- LabeledShape -- LabeledShapeRequest -- LabeledTrack -- LabeledTrackRequest -- LocationEnum -- LogEvent -- LogEventRequest -- LoginRequest -- Manifest -- ManifestRequest -- MembershipRead -- MembershipWrite -- MetaUser -- OperationStatus -- OrganizationRead -- OrganizationWrite -- OrganizationWriteRequest -- PaginatedCloudStorageReadList -- PaginatedCommentReadList -- PaginatedInvitationReadList -- PaginatedIssueReadList -- PaginatedJobCommitList -- PaginatedJobReadList -- PaginatedMembershipReadList -- PaginatedMetaUserList -- PaginatedPolymorphicProjectList -- PaginatedTaskReadList -- PasswordChangeRequest -- PasswordResetConfirmRequest -- PasswordResetSerializerExRequest -- PatchedCloudStorageWriteRequest -- PatchedCommentWriteRequest -- PatchedDataMetaWriteRequest -- PatchedInvitationWriteRequest -- PatchedIssueWriteRequest -- PatchedJobWriteRequest -- PatchedLabelRequest -- PatchedLabeledDataRequest -- PatchedMembershipWriteRequest -- PatchedOrganizationWriteRequest -- PatchedProjectWriteRequest -- PatchedProjectWriteRequestTargetStorage -- PatchedTaskWriteRequest -- PatchedTaskWriteRequestTargetStorage -- PatchedUserRequest -- Plugins -- PolymorphicProject -- ProjectFileRequest -- ProjectRead -- ProjectReadAssignee -- ProjectReadOwner -- ProjectReadTargetStorage -- ProjectSearch -- ProjectWriteRequest -- ProviderTypeEnum -- RestAuthDetail -- RestrictedRegister -- RestrictedRegisterRequest -- RoleEnum -- RqStatus -- RqStatusStateEnum -- Segment -- ShapeType -- SigningRequest -- SimpleJob -- SortingMethod -- Storage -- StorageMethod -- StorageRequest -- StorageType -- SubLabeledShape -- SubLabeledShapeRequest -- SubLabeledTrack -- SubLabeledTrackRequest -- Sublabel -- SublabelRequest -- TaskAnnotationsUpdateRequest -- TaskAnnotationsWriteRequest -- TaskFileRequest -- TaskRead -- TaskReadTargetStorage -- TaskWriteRequest -- Token -- TrackedShape -- TrackedShapeRequest -- User -- UserAgreement -- UserAgreementRequest +Model parameters can be passed as models, or as plain Python data structures. This rule applies +recursively, starting from the method parameters. In particular, this means you can pass +a dict into a method or into a model constructor, and corresponding fields will +be parsed from this data automatically: + +```python +task_spec = models.TaskWriteRequest( + name='example task', + labels=[ + models.PatchedLabelRequest( + name="car", + color="#ff00ff", + attributes=[ + model.AttributeRequest( + name="a", + mutable=True, + input_type="number", + default_value="5", + values=["4", "5", "6"] + ) + ] + ) + ], +) +api_client.tasks_api.create(task_spec) +``` + +Is equivalent to: + +```python +api_client.tasks_api.create({ + 'name': 'example task', + "labels": [{ + "name": "car", + "color": "#ff00ff", + "attributes": [ + { + "name": "a", + "mutable": True, + "input_type": "number", + "default_value": "5", + "values": ["4", "5", "6"] + } + ] + }], +}) +``` + +You can mix these variants. + +Most models provide corresponding interface classes called like `I`. They can be +used to implement your own classes or describe APIs. They just provide type annotations +and descriptions for model fields. + +You can export model values to plain Python dicts using the `as_dict()` method and +the `cvat_sdk.api_client.model_utils.to_json()` function. + +You can find the list of the available models and their documentation [here](../reference/models/). + +## Sending requests + +To send a request to a server endpoint, you need to obtain an instance of the corresponding `*Api` +class. You can find summary about available API classes and supported endpoints +[here](../reference/apis). The `*Api` instance object allows to send requests to the relevant +server endpoints. + +By default, all operations return 2 objects: the parsed response data and the response itself. +A typical call looks like this: + +```python +from cvat_sdk.api_client import ApiClient, apis + +with ApiClient(...) as api_client: + ... + (data, response) = api_client.tasks_api.list() + # process the response ... +``` + +Operation parameters can be passed as positional or keyword arguments. There are also several +extra arguments which change invocation logic: + +- `_parse_response` - Allows to enable and disable response data parsing. When enabled, + the response data is parsed into a model or a basic type and returned as the first value. + When disabled, the response is not parsed, and `None` is returned. Can be useful, + for instance, if you need to parse data manually, or if you expect an error in the response. +- `_check_status` - Allows to enable or disable response status checks. When enabled, the + response status code is checked to be positive as defined in the [HTTP standards](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes). + In case of negative status, an exception is raised. +- `_request_timeout` - Allows to control timeout +- `_content_type` - Allows to specify the `Content-Type` header value for the request. Endpoints + can support different content types and behave differently depending on the value. For file + uploads `_content_type="multipart/form-data"` must be specified. + +> **NOTE**: the API is autogenerated. In some cases the server API schema may be incomplete +or underspecified. Please report to us all the problems found. A typical problem is that a +response data can't be parsed automatically due to the incorrect schema. In this case, the +simplest workaround is to disable response parsing using the `_parse_response=False` +method argument. + +You can find many examples of API client usage in REST API tests [here](https://github.com/opencv/cvat/tree/develop/tests/python). + +### Organizations + +To call an operation in the context of an organization, use one of these method arguments: + +- `org` - The unique organization slug +- `org_id`- The organization id + +```python +... +(updated_annotations, response) = api_client.tasks_api.partial_update_annotations( + id=task_id, + org_id=org_id, + action='update', + patched_labeled_data_request=data +) +``` + +### Paginated responses + +There are several endpoints that allow to request multiple server entities. Typically, these +endpoints are called `list_...`. When there are lots of data, the responses can be paginated to +reduce server load. If an endpoint returns paginated data, a single page is returned per request. +In some cases all entries need to be retrieved. CVAT doesn't provide specific API or parameters +for this, so the solution is to write a loop to collect and join data from multiple requests. +SDK provides an utility function for this at `cvat_sdk.core.helpers.get_paginated_collection()`. + +Example: + +```python +from cvat_sdk.core.helpers import get_paginated_collection + +... +project_tasks = get_paginated_collection( + api_client.projects_api.list_tasks_endpoint, + id=project_id, +) +``` diff --git a/site/content/en/docs/api_sdk/sdk/reference/_index.md b/site/content/en/docs/api_sdk/sdk/reference/_index.md new file mode 100644 index 000000000..e91695800 --- /dev/null +++ b/site/content/en/docs/api_sdk/sdk/reference/_index.md @@ -0,0 +1,6 @@ +--- +title: "SDK API Reference" +linkTitle: "API Reference" +weight: 1 +description: '' +--- diff --git a/site/content/en/docs/api_sdk/sdk/reference/apis/.gitignore b/site/content/en/docs/api_sdk/sdk/reference/apis/.gitignore new file mode 100644 index 000000000..d21dcbb62 --- /dev/null +++ b/site/content/en/docs/api_sdk/sdk/reference/apis/.gitignore @@ -0,0 +1,2 @@ +# The files are autogenerated here +*.md diff --git a/site/content/en/docs/api_sdk/sdk/reference/models/.gitignore b/site/content/en/docs/api_sdk/sdk/reference/models/.gitignore new file mode 100644 index 000000000..3b4f48f54 --- /dev/null +++ b/site/content/en/docs/api_sdk/sdk/reference/models/.gitignore @@ -0,0 +1,3 @@ +# The files are autogenerated here +*.md +!_index.md diff --git a/site/content/en/docs/api_sdk/sdk/reference/models/_index.md b/site/content/en/docs/api_sdk/sdk/reference/models/_index.md new file mode 100644 index 000000000..ef978c2f3 --- /dev/null +++ b/site/content/en/docs/api_sdk/sdk/reference/models/_index.md @@ -0,0 +1,6 @@ +--- +title: 'Models' +linkTitle: 'Models' +weight: 1 +description: '' +--- diff --git a/site/process_sdk_docs.py b/site/process_sdk_docs.py new file mode 100755 index 000000000..ac20a2d8d --- /dev/null +++ b/site/process_sdk_docs.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python + +# Copyright (C) 2022 CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +import argparse +import os +import os.path as osp +import re +import shutil +import sys +import textwrap +from glob import iglob +from typing import Callable, List + +from inflection import underscore + + +class Processor: + _reference_files: List[str] + + def __init__(self, *, input_dir: str, site_root: str) -> None: + self._input_dir = input_dir + self._site_root = site_root + + self._content_dir = osp.join(self._site_root, "content") + self._sdk_reference_dir = osp.join( + self._content_dir, "en/docs/api_sdk/sdk/reference" + ) + self._templates_dir = osp.join(self._site_root, "templates") + + @staticmethod + def _copy_files(src_dir: str, glob_pattern: str, dst_dir: str) -> List[str]: + copied_files = [] + + for src_path in iglob(osp.join(src_dir, glob_pattern), recursive=True): + src_filename = osp.relpath(src_path, src_dir) + dst_path = osp.join(dst_dir, src_filename) + # assume dst dir exists + shutil.copy(src_path, dst_path, follow_symlinks=True) + + copied_files.append(dst_path) + + return copied_files + + def _copy_pages(self): + self._reference_files = self._copy_files( + self._input_dir, "*/**/*.md", self._sdk_reference_dir + ) + + def _add_page_headers(self): + """ + Adds headers required by hugo to docs pages + """ + + HEADER_SEPARATOR = "---" + + for p in self._reference_files: + with open(p) as f: + contents = f.read() + + assert not contents.startswith(HEADER_SEPARATOR), p + + lines = contents.splitlines() + + assert lines[0].startswith("#") + classname = lines[0][1:].strip() + + header = textwrap.dedent( + """\ + %(header_separator)s + title: '%(classname)s class reference' + linkTitle: '%(classname)s' + weight: 10 + description: '' + %(header_separator)s + """ + % {"header_separator": HEADER_SEPARATOR, "classname": classname} + ) + + contents = header + "\n".join(lines[1:]) + + with open(p, "w") as f: + f.write(contents) + + def _extract_apis_summary(self, readme_path: str) -> str: + with open(readme_path) as f: + readme_contents = f.read() + + apis_summary = re.search( + r"## Available API Endpoints(.*)## Available Models", + readme_contents, + flags=re.DOTALL, + )[1] + assert len(apis_summary) > 0 + + return apis_summary + + def _move_api_summary(self): + """ + Moves API summary section from README to apis/_index + """ + + SUMMARY_REPLACE_TOKEN = "{{REPLACEME:apis_summary}}" # nosec + + apis_summary = self._extract_apis_summary( + osp.join(self._input_dir, "README.md") + ) + + apis_index_filename = osp.join( + osp.relpath(self._sdk_reference_dir, self._content_dir), "apis/_index.md" + ) + apis_index_path = osp.join( + self._templates_dir, apis_index_filename + ".template" + ) + with open(apis_index_path) as f: + contents = f.read() + + contents = contents.replace(SUMMARY_REPLACE_TOKEN, apis_summary) + + with open(osp.join(self._content_dir, apis_index_filename), "w") as f: + f.write(contents) + + def _fix_page_links_and_references(self): + """ + Replaces reference page links from full lowercase (which is generated by hugo from the + orignal camelcase and creates broken links) ('authapi') to the minus-case ('auth-api'), + which is more readable and works. + Adds an extra parent directory part to links ('../') as hugo requires, even for neighbor + files. + """ + + mapping = {} + + for src_path in self._reference_files: + src_filename = osp.relpath(src_path, self._sdk_reference_dir) + dst_filename = underscore(src_filename).replace("_", "-") + dst_path = osp.join(self._sdk_reference_dir, dst_filename) + os.rename(src_path, dst_path) + mapping[src_filename] = dst_filename + + self._reference_files = [ + osp.join(self._sdk_reference_dir, p) for p in mapping.values() + ] + + for p in iglob(self._sdk_reference_dir + "/**/*.md", recursive=True): + with open(p) as f: + contents = f.read() + + for src_filename, dst_filename in mapping.items(): + src_dir, src_filename = osp.split(osp.splitext(src_filename)[0]) + dst_filename = osp.basename(osp.splitext(dst_filename)[0]) + contents = re.sub( + rf"(\[.*?\]\()((?:\.\./)?(?:{src_dir}/)?){src_filename}((?:#[^\)]*?)?\))", + rf"\1../\2{dst_filename}\3", + contents, + ) + + with open(p, "w") as f: + f.write(contents) + + def _process_non_code_blocks( + self, text: str, handlers: List[Callable[[str], str]] + ) -> str: + """ + Allows to process Markdown documents with passed callbacks. Callbacks are only + executed outside code blocks. + """ + + used_quotes = "" + block_start_pos = 0 + inside_code_block = False + while block_start_pos < len(text): + pattern = re.compile(used_quotes or "```|`") + next_code_block_quote = pattern.search(text, pos=block_start_pos) + if next_code_block_quote is not None: + if not used_quotes: + inside_code_block = False + block_end_pos = next_code_block_quote.start(0) + used_quotes = next_code_block_quote.group(0) + else: + inside_code_block = True + block_end_pos = next_code_block_quote.end(0) + used_quotes = None + else: + block_end_pos = len(text) + + if not inside_code_block: + block = text[block_start_pos:block_end_pos] + + for handler in handlers: + block = handler(block) + + text = text[:block_start_pos] + block + text[block_end_pos:] + block_end_pos = block_start_pos + len(block) + len(used_quotes) + + block_start_pos = block_end_pos + + return text + + def _escape_free_square_brackets(self, text: str) -> str: + return re.sub(r"\[([^\[\]]*?)\]([^\(])", r"\[\1\]\2", text) + + def _add_angle_brackets_to_free_links(self, text: str) -> str: + # Adapted from https://stackoverflow.com/a/31952097 + URL_REGEX = ( + # Scheme (HTTP, HTTPS): + r"(?:https?:\/\/)" + r"(?:" + # www: + r"(?:www\.)?" + # Host and domain (including ccSLD): + r"(?:(?:[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.)+)" + # TLD: + r"(?:[a-zA-Z]{2,6})" + # IP Address: + r"|(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" + r")" + # Port: + r"(?::\d{1,5})?" + # Query path: + r"(?:(?:\/\S+)*|\/)" + ) + + text = re.sub( + r"(\A|[\.\s])(" + URL_REGEX + r")([\.\s]|\Z)", + r"\1<\2>\3", + text, + flags=re.MULTILINE, + ) + + return text + + def _fix_parsing_problems(self): + """ + Adds angle brackets to freestanding links, as the linter requires. Such links can appear + from the generated model and api descriptions. + Adds escapes to freestanding square brackets to make parsing correct. + """ + + for p in iglob(self._sdk_reference_dir + "/**/*.md", recursive=True): + with open(p) as f: + contents = f.read() + + contents = self._process_non_code_blocks( + contents, + [ + self._add_angle_brackets_to_free_links, + self._escape_free_square_brackets, + ], + ) + + with open(p, "w") as f: + f.write(contents) + + def run(self): + assert osp.isdir(self._input_dir), self._input_dir + assert osp.isdir(self._site_root), self._site_root + assert osp.isdir(self._sdk_reference_dir), self._sdk_reference_dir + assert osp.isdir(self._templates_dir), self._templates_dir + + self._copy_pages() + self._move_api_summary() + self._add_page_headers() + self._fix_page_links_and_references() + self._fix_parsing_problems() + + +def parse_args(args=None): + parser = argparse.ArgumentParser() + parser.add_argument( + "--input-dir", + type=osp.abspath, + default="cvat-sdk/docs/", + help="Path to the cvat-sdk/docs/ directory", + ) + parser.add_argument( + "--site-root", + type=osp.abspath, + default="site/", + ) + + return parser.parse_args(args) + + +def main(args=None): + args = parse_args(args) + processor = Processor(input_dir=args.input_dir, site_root=args.site_root) + processor.run() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/site/requirements.txt b/site/requirements.txt index d08175590..8f5c22668 100644 --- a/site/requirements.txt +++ b/site/requirements.txt @@ -1,3 +1,6 @@ +black>=22.1.0 gitpython +inflection >= 0.5.1 +isort>=5.10.1 packaging -toml \ No newline at end of file +toml diff --git a/site/templates/en/docs/api_sdk/sdk/reference/apis/_index.md.template b/site/templates/en/docs/api_sdk/sdk/reference/apis/_index.md.template new file mode 100644 index 000000000..0e2d35579 --- /dev/null +++ b/site/templates/en/docs/api_sdk/sdk/reference/apis/_index.md.template @@ -0,0 +1,8 @@ +--- +title: 'APIs' +linkTitle: 'APIs' +weight: 1 +description: '' +--- + +{{REPLACEME:apis_summary}} -- GitLab