From 9b63b4b2e64255debf0e271aca743abbc1c1ed1f Mon Sep 17 00:00:00 2001 From: Adrian Chang Date: Tue, 19 Mar 2024 20:03:58 -0700 Subject: [PATCH] multi line --- .github/workflows/publish.yaml | 157 +- .github/workflows/python-package.yml | 70 +- CONTRIBUTING.md | 10 + Dockerfile | 2 +- README.md | 2 +- docs/Makefile | 20 - docs/{source => }/conf.py | 22 +- docs/labelbox/annotation-import.rst | 7 + docs/labelbox/app.rst | 6 + docs/labelbox/asset-attachment.rst | 6 + docs/labelbox/batch.rst | 6 + docs/labelbox/benchmark.rst | 6 + docs/labelbox/bulk-import-request.rst | 7 + docs/labelbox/client.rst | 8 + .../data/__init__.py => docs/labelbox/conf.py | 0 .../labelbox/conflict-resolution-strategy.rst | 5 + docs/labelbox/datarow-metadata.rst | 7 + docs/labelbox/datarow.rst | 5 + docs/labelbox/dataset.rst | 6 + docs/labelbox/enums.rst | 6 + docs/labelbox/exceptions.rst | 6 + docs/labelbox/export-task.rst | 7 + docs/labelbox/foundry-client.rst | 6 + docs/labelbox/foundry-model.rst | 6 + docs/labelbox/identifiable.rst | 6 + docs/labelbox/identifiables.rst | 6 + docs/labelbox/index.rst | 44 + docs/labelbox/label.rst | 6 + docs/labelbox/labeling-frontend-options.rst | 8 + docs/labelbox/labeling-frontend.rst | 7 + docs/labelbox/labeling-parameter-override.rst | 7 + docs/labelbox/model-run.rst | 6 + docs/labelbox/model.rst | 6 + docs/labelbox/ontology.rst | 7 + docs/labelbox/organization.rst | 6 + docs/labelbox/pagination.rst | 7 + docs/labelbox/project.rst | 7 + docs/labelbox/quality-mode.rst | 6 + docs/labelbox/resource-tag.rst | 6 + docs/labelbox/review.rst | 6 + docs/labelbox/send-to-annotate-params.rst | 6 + docs/labelbox/slice.rst | 6 + docs/labelbox/task-queue.rst | 6 + docs/labelbox/task.rst | 6 + docs/labelbox/user.rst | 6 + docs/labelbox/webhook.rst | 6 + docs/make.bat | 35 - docs/requirements.txt | 2 - docs/source/_static/js/prevent_collapse.js | 13 - docs/source/index.rst | 266 --- libs/labelbox/pyproject.toml | 5 +- libs/labelbox/tests/conftest.py | 1025 +++++++++- .../tests/data/annotation_import/conftest.py | 1717 +++++++++++------ .../annotation_import/fixtures/__init__.py | 0 .../annotation_import/fixtures/export_v2.py | 446 ----- .../fixtures/video_annotations.py | 49 - .../data/annotation_import/test_data_types.py | 470 +++-- .../fixtures/annotations.py => conftest.py} | 2 +- libs/labelbox/tests/integration/conftest.py | 752 +------- libs/labelbox/tests/integration/test_batch.py | 10 +- .../integration/test_delegated_access.py | 5 +- .../tests/integration/test_ephemeral.py | 10 +- .../tests/integration/test_foundry.py | 10 +- pyproject.toml | 6 + requirements-dev.lock | 37 + requirements.lock | 45 + 66 files changed, 2982 insertions(+), 2483 deletions(-) delete mode 100644 docs/Makefile rename docs/{source => }/conf.py (87%) create mode 100644 docs/labelbox/annotation-import.rst create mode 100644 docs/labelbox/app.rst create mode 100644 docs/labelbox/asset-attachment.rst create mode 100644 docs/labelbox/batch.rst create mode 100644 docs/labelbox/benchmark.rst create mode 100644 docs/labelbox/bulk-import-request.rst create mode 100644 docs/labelbox/client.rst rename libs/labelbox/tests/data/__init__.py => docs/labelbox/conf.py (100%) create mode 100644 docs/labelbox/conflict-resolution-strategy.rst create mode 100644 docs/labelbox/datarow-metadata.rst create mode 100644 docs/labelbox/datarow.rst create mode 100644 docs/labelbox/dataset.rst create mode 100644 docs/labelbox/enums.rst create mode 100644 docs/labelbox/exceptions.rst create mode 100644 docs/labelbox/export-task.rst create mode 100644 docs/labelbox/foundry-client.rst create mode 100644 docs/labelbox/foundry-model.rst create mode 100644 docs/labelbox/identifiable.rst create mode 100644 docs/labelbox/identifiables.rst create mode 100644 docs/labelbox/index.rst create mode 100644 docs/labelbox/label.rst create mode 100644 docs/labelbox/labeling-frontend-options.rst create mode 100644 docs/labelbox/labeling-frontend.rst create mode 100644 docs/labelbox/labeling-parameter-override.rst create mode 100644 docs/labelbox/model-run.rst create mode 100644 docs/labelbox/model.rst create mode 100644 docs/labelbox/ontology.rst create mode 100644 docs/labelbox/organization.rst create mode 100644 docs/labelbox/pagination.rst create mode 100644 docs/labelbox/project.rst create mode 100644 docs/labelbox/quality-mode.rst create mode 100644 docs/labelbox/resource-tag.rst create mode 100644 docs/labelbox/review.rst create mode 100644 docs/labelbox/send-to-annotate-params.rst create mode 100644 docs/labelbox/slice.rst create mode 100644 docs/labelbox/task-queue.rst create mode 100644 docs/labelbox/task.rst create mode 100644 docs/labelbox/user.rst create mode 100644 docs/labelbox/webhook.rst delete mode 100644 docs/make.bat delete mode 100644 docs/requirements.txt delete mode 100644 docs/source/_static/js/prevent_collapse.js delete mode 100644 docs/source/index.rst delete mode 100644 libs/labelbox/tests/data/annotation_import/fixtures/__init__.py delete mode 100644 libs/labelbox/tests/data/annotation_import/fixtures/export_v2.py delete mode 100644 libs/labelbox/tests/data/annotation_import/fixtures/video_annotations.py rename libs/labelbox/tests/data/{annotation_import/fixtures/annotations.py => conftest.py} (100%) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 3637ebb0ea..15c81963ee 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,46 +1,118 @@ -# Triggers a pypi publication when a release is created - -name: Publish Python Package +name: Labelbox Python SDK Publish on: release: types: [created] +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + jobs: build: runs-on: ubuntu-latest steps: + - name: Cancel previous workflow + uses: styfle/cancel-workflow-action@0.12.1 + with: + access_token: ${{ github.token }} - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 with: - python-version: '3.x' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel - - - name: Build + token: ${{ secrets.ACTIONS_ACCESS_TOKEN }} + ref: ${{ github.head_ref }} + - name: Install the latest version of rye + uses: eifinger/setup-rye@v2 + with: + version: '0.29.0' + enable-cache: true + - name: Rye Setup run: | - python setup.py sdist bdist_wheel - + rye config --set-bool behavior.use-uv=true - name: Create build working-directory: libs/labelbox - run: rye build - + run: | + rye sync + rye build + - uses: actions/upload-artifact@v4 + with: + name: build + path: ./dist + test-build: + needs: ['build'] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - python-version: 3.8 + prod-key: PROD_LABELBOX_API_KEY_2 + staging-key: STAGING_LABELBOX_API_KEY_2 + da-test-key: DA_GCP_LABELBOX_API_KEY + - python-version: 3.9 + prod-key: PROD_LABELBOX_API_KEY_3 + staging-key: STAGING_LABELBOX_API_KEY_3 + da-test-key: DA_GCP_LABELBOX_API_KEY + - python-version: "3.10" + prod-key: PROD_LABELBOX_API_KEY_4 + staging-key: STAGING_LABELBOX_API_KEY_4 + da-test-key: DA_GCP_LABELBOX_API_KEY + - python-version: 3.11 + prod-key: LABELBOX_API_KEY + staging-key: STAGING_LABELBOX_API_KEY + da-test-key: DA_GCP_LABELBOX_API_KEY + - python-version: 3.12 + prod-key: PROD_LABELBOX_API_KEY_5 + staging-key: STAGING_LABELBOX_API_KEY_5 + da-test-key: DA_GCP_LABELBOX_API_KEY + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.ACTIONS_ACCESS_TOKEN }} + ref: ${{ github.head_ref }} + - name: Install the latest version of rye + uses: eifinger/setup-rye@v2 + with: + version: '0.29.0' + enable-cache: true + - name: Rye Setup + run: | + rye config --set-bool behavior.use-uv=true + - name: Python setup + run: rye pin ${{ matrix.python-version }} + - uses: actions/download-artifact@v4 + with: + name: build + path: ./dist - name: Prepare package and environment run: | + rye sync rye run toml unset --toml-path pyproject.toml tool.rye.workspace - rye sync -f + rye sync -f --update-all + - name: Integration Testing + env: + PYTEST_XDIST_AUTO_NUM_WORKERS: 32 + LABELBOX_TEST_API_KEY: ${{ secrets[matrix.prod-key] }} + DA_GCP_LABELBOX_API_KEY: ${{ secrets[matrix.da-test-key] }} + LABELBOX_TEST_ENVIRON: prod + run: | rye add labelbox --path ./$(find ./dist/ -name *.tar.gz) --sync - - - uses: actions/upload-artifact@v4 - with: - path: ./dist - + cd libs/labelbox + rm pyproject.toml + rye run pytest tests/integration + - name: Data Testing + env: + PYTEST_XDIST_AUTO_NUM_WORKERS: 32 + LABELBOX_TEST_API_KEY: ${{ secrets[matrix.prod-key] }} + DA_GCP_LABELBOX_API_KEY: ${{ secrets[matrix.da-test-key] }} + LABELBOX_TEST_ENVIRON: prod + run: | + rye add labelbox --path ./$(find ./dist/ -name *.tar.gz) --sync --features data + cd libs/labelbox + rye run pytest tests/data pypi-publish: - needs: ['build'] + needs: ['test-build'] environment: name: publish url: 'https://pypi.org/project/labelbox/' @@ -50,10 +122,43 @@ jobs: id-token: write steps: - uses: actions/download-artifact@v4 - + with: + name: build - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: artifact/ - + github-pages: + needs: ['test-build'] + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.ACTIONS_ACCESS_TOKEN }} + ref: ${{ github.head_ref }} + - name: Install the latest version of rye + uses: eifinger/setup-rye@v2 + with: + version: '0.29.0' + enable-cache: true + - name: Rye Setup + run: | + rye config --set-bool behavior.use-uv=true + - name: Create build + run: | + rye sync + rye run docs + - name: Setup Pages + uses: actions/configure-pages@v4 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: 'dist/' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 # Note that the build and pypi-publish jobs are split so that the additional permissions are only granted to the pypi-publish job. diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9aa96ebab6..752c59554a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -19,23 +19,18 @@ jobs: include: - python-version: 3.8 prod-key: PROD_LABELBOX_API_KEY_2 - staging-key: STAGING_LABELBOX_API_KEY_2 - da-test-key: DA_GCP_LABELBOX_API_KEY - - python-version: 3.9 - prod-key: PROD_LABELBOX_API_KEY_3 - staging-key: STAGING_LABELBOX_API_KEY_3 - da-test-key: DA_GCP_LABELBOX_API_KEY - - python-version: "3.10" - prod-key: PROD_LABELBOX_API_KEY_4 - staging-key: STAGING_LABELBOX_API_KEY_4 da-test-key: DA_GCP_LABELBOX_API_KEY + #- python-version: 3.9 + # prod-key: PROD_LABELBOX_API_KEY_3 + # da-test-key: DA_GCP_LABELBOX_API_KEY + #- python-version: "3.10" + # prod-key: PROD_LABELBOX_API_KEY_4 + # da-test-key: DA_GCP_LABELBOX_API_KEY - python-version: 3.11 prod-key: LABELBOX_API_KEY - staging-key: STAGING_LABELBOX_API_KEY da-test-key: DA_GCP_LABELBOX_API_KEY - python-version: 3.12 prod-key: PROD_LABELBOX_API_KEY_5 - staging-key: STAGING_LABELBOX_API_KEY_5 da-test-key: DA_GCP_LABELBOX_API_KEY steps: - name: Cancel previous workflow @@ -59,14 +54,10 @@ jobs: - name: Environment setup working-directory: libs/labelbox run: | - rye lock --update-all - rye sync -f + rye sync -f --update-all - name: Linting working-directory: libs/labelbox run: rye run lint - - name: Unit Testing - working-directory: libs/labelbox - run: rye run unit - name: Integration Testing env: PYTEST_XDIST_AUTO_NUM_WORKERS: 32 @@ -74,13 +65,50 @@ jobs: DA_GCP_LABELBOX_API_KEY: ${{ secrets[matrix.da-test-key] }} working-directory: libs/labelbox run: rye run integration - - name: Data Testing + - name: Unit && Data Testing env: PYTEST_XDIST_AUTO_NUM_WORKERS: 32 LABELBOX_TEST_API_KEY: ${{ secrets[matrix.prod-key] }} DA_GCP_LABELBOX_API_KEY: ${{ secrets[matrix.da-test-key] }} working-directory: libs/labelbox - run: > - rye sync -f --features data - rye lock --update-all - rye run data \ No newline at end of file + run: | + rye sync -f --features labelbox/data + rye run unit + rye run data + test-pypi: + runs-on: ubuntu-latest + needs: ['build'] + environment: + name: Test-PyPI + url: 'https://test.pypi.org/p/labelbox-test' + permissions: + # IMPORTANT: this permission is mandatory for trusted publishing + id-token: write + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.ACTIONS_ACCESS_TOKEN }} + ref: ${{ github.head_ref }} + - name: Install the latest version of rye + uses: eifinger/setup-rye@v2 + with: + version: '0.29.0' + enable-cache: true + - name: Rye Setup + run: | + rye config --set-bool behavior.use-uv=true + - name: Create build + id: create-build + working-directory: libs/labelbox + run: | + VERSION=$(date +"%Y.%m.%d.%H.%M") + echo "pip install --index-url https://test.pypi.org/simple/ labelbox-test@$VERSION" >> "$GITHUB_STEP_SUMMARY" + rye sync + rye version "$VERSION" + rye run toml set --toml-path pyproject.toml project.name labelbox-test + rye build + - name: Publish package distributions to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + repository-url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1bae0347ac..aadeca2072 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,8 @@ To understand why Rye was chosen, see [here](https://alpopkes.com/posts/python/p If you want to not deal with setting up `Rye` on your local machine directly, feel free to use one of [Docker containers](https://github.com/Labelbox/labelbox-python/pkgs/container/labelbox-python) we have built which contains the entire SDK with `Rye` setup for you already. +**You can use Poetry to manage the virtual environment.** There's nothing blocking you from using Poetry to manage the virtual environment as the standard `pyproject.toml` format is used, but you'll have to deal with managing Python yourself + be asked not to check in any `poetry.lock` files. Also, you'll have to run equivalent poetry commands that may not be listed in the documentation to perform the same general operations (testing, building, etc.). + ## Building Locally These are general steps that all modules in `libs/` adhere to give the prerequisite of the installation of `Rye`. @@ -89,6 +91,14 @@ You may also manually format your code by running the following: rye run lint ``` +### Documentation + +To generate `ReadTheDocs,` run the following command. + +```bash +rye run docs +``` + ## Jupyter Notebooks We have samples in the `examples` directory and using them for testing can help increase your productivity. diff --git a/Dockerfile b/Dockerfile index d128fe3166..f1ed2bfd41 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,4 +31,4 @@ RUN rye config --set-bool behavior.global-python=true && \ rye config --set-bool behavior.use-uv=true && \ rye sync -CMD ["rye", "run", "pytest", "libs/labelbox/tests/integration libs/labelbox/tests/data"] \ No newline at end of file +CMD cd libs/labelbox && rye run integration && rye sync -f --features labelbox/data && rye run unit && rye run data \ No newline at end of file diff --git a/README.md b/README.md index 6aa88f3a7b..faa1ecbac5 100644 --- a/README.md +++ b/README.md @@ -72,4 +72,4 @@ The SDK is well-documented to help developers get started quickly and use the SD - [Labelbox Official Documentation](https://docs.labelbox.com/docs/overview) - [Jupyter Notebook Examples](https://github.com/Labelbox/labelbox-python/tree/master/examples) -- [Read The Docs](https://labelbox-python.readthedocs.io/en/latest/) \ No newline at end of file +- [Github Pages](https://labelbox.github.io/labelbox-python/) \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf102..0000000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/source/conf.py b/docs/conf.py similarity index 87% rename from docs/source/conf.py rename to docs/conf.py index 41a63a0468..961c750029 100644 --- a/docs/source/conf.py +++ b/docs/conf.py @@ -10,17 +10,12 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os -import sys - -sys.path.insert(0, os.path.abspath('../..')) # -- Project information ----------------------------------------------------- project = 'Python SDK reference' -copyright = '2021, Labelbox' +copyright = '2024, Labelbox' author = 'Labelbox' - release = '3.66.0' # -- General configuration --------------------------------------------------- @@ -29,7 +24,11 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon' + 'multiproject', + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', + 'sphinx_rtd_theme' ] # Add any paths that contain templates here, relative to this directory. @@ -40,6 +39,12 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +multiproject_projects = { + "labelbox": { + "path": "labelbox" + } +} + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -50,10 +55,9 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = [] # Prevent the sidebar from collapsing -html_js_files = ['js/prevent_collapse.js'] html_theme_options = { "collapse_navigation": False, } diff --git a/docs/labelbox/annotation-import.rst b/docs/labelbox/annotation-import.rst new file mode 100644 index 0000000000..8d671fb57b --- /dev/null +++ b/docs/labelbox/annotation-import.rst @@ -0,0 +1,7 @@ +Annotation Import +=============================================================================================== + +.. automodule:: labelbox.schema.annotation_import + :members: + :show-inheritance: + diff --git a/docs/labelbox/app.rst b/docs/labelbox/app.rst new file mode 100644 index 0000000000..07b2145c48 --- /dev/null +++ b/docs/labelbox/app.rst @@ -0,0 +1,6 @@ +App +=============================================================================================== + +.. automodule:: labelbox.schema.foundry.app + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/asset-attachment.rst b/docs/labelbox/asset-attachment.rst new file mode 100644 index 0000000000..5ae4dc58fd --- /dev/null +++ b/docs/labelbox/asset-attachment.rst @@ -0,0 +1,6 @@ +Asset Attachment +=============================================================================================== + +.. automodule:: labelbox.schema.asset_attachment + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/batch.rst b/docs/labelbox/batch.rst new file mode 100644 index 0000000000..cffab349fb --- /dev/null +++ b/docs/labelbox/batch.rst @@ -0,0 +1,6 @@ +Batch +=============================================================================================== + +.. automodule:: labelbox.schema.batch + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/benchmark.rst b/docs/labelbox/benchmark.rst new file mode 100644 index 0000000000..f894d2eff2 --- /dev/null +++ b/docs/labelbox/benchmark.rst @@ -0,0 +1,6 @@ +Benchmark +=============================================================================================== + +.. automodule:: labelbox.schema.benchmark + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/bulk-import-request.rst b/docs/labelbox/bulk-import-request.rst new file mode 100644 index 0000000000..3a049743c5 --- /dev/null +++ b/docs/labelbox/bulk-import-request.rst @@ -0,0 +1,7 @@ +Bulk Import Request +=============================================================================================== + +.. automodule:: labelbox.schema.bulk_import_request + :members: + :exclude-members: create_from_local_file, create_from_objects, create_from_url, from_name + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/client.rst b/docs/labelbox/client.rst new file mode 100644 index 0000000000..20bc3b8cb8 --- /dev/null +++ b/docs/labelbox/client.rst @@ -0,0 +1,8 @@ +Client +=============================================================================================== + +.. automodule:: labelbox.client + :members: + :special-members: __init__ + :exclude-members: upload_data, upload_file + :show-inheritance: \ No newline at end of file diff --git a/libs/labelbox/tests/data/__init__.py b/docs/labelbox/conf.py similarity index 100% rename from libs/labelbox/tests/data/__init__.py rename to docs/labelbox/conf.py diff --git a/docs/labelbox/conflict-resolution-strategy.rst b/docs/labelbox/conflict-resolution-strategy.rst new file mode 100644 index 0000000000..48af9439c5 --- /dev/null +++ b/docs/labelbox/conflict-resolution-strategy.rst @@ -0,0 +1,5 @@ +Conflict Resolution Strategy +=============================================================================================== +.. automodule:: labelbox.schema.conflict_resolution_strategy + :members: + :show-inheritance: diff --git a/docs/labelbox/datarow-metadata.rst b/docs/labelbox/datarow-metadata.rst new file mode 100644 index 0000000000..add5a62ca2 --- /dev/null +++ b/docs/labelbox/datarow-metadata.rst @@ -0,0 +1,7 @@ +DataRow Metadata +=============================================================================================== + +.. automodule:: labelbox.schema.data_row_metadata + :members: + :exclude-members: _DeleteBatchDataRowMetadata + :show-inheritance: diff --git a/docs/labelbox/datarow.rst b/docs/labelbox/datarow.rst new file mode 100644 index 0000000000..72092291df --- /dev/null +++ b/docs/labelbox/datarow.rst @@ -0,0 +1,5 @@ +DataRow +=============================================================================================== +.. automodule:: labelbox.schema.data_row + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/dataset.rst b/docs/labelbox/dataset.rst new file mode 100644 index 0000000000..c1d26b3859 --- /dev/null +++ b/docs/labelbox/dataset.rst @@ -0,0 +1,6 @@ +Dataset +=============================================================================================== + +.. automodule:: labelbox.schema.dataset + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/enums.rst b/docs/labelbox/enums.rst new file mode 100644 index 0000000000..e9820a1452 --- /dev/null +++ b/docs/labelbox/enums.rst @@ -0,0 +1,6 @@ +Enums +=============================================================================================== + +.. automodule:: labelbox.schema.enums + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/exceptions.rst b/docs/labelbox/exceptions.rst new file mode 100644 index 0000000000..3082bc0815 --- /dev/null +++ b/docs/labelbox/exceptions.rst @@ -0,0 +1,6 @@ +Exceptions +=============================================================================================== + +.. automodule:: labelbox.exceptions + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/export-task.rst b/docs/labelbox/export-task.rst new file mode 100644 index 0000000000..8b1bf647dd --- /dev/null +++ b/docs/labelbox/export-task.rst @@ -0,0 +1,7 @@ +Export Task +=============================================================================================== + +.. automodule:: labelbox.schema.export_task + :members: + :exclude-members: FileRetrieverByLine, FileRetrieverByOffset, FileRetrieverStrategy, Range, Converter + :show-inheritance: diff --git a/docs/labelbox/foundry-client.rst b/docs/labelbox/foundry-client.rst new file mode 100644 index 0000000000..b9ef76c48f --- /dev/null +++ b/docs/labelbox/foundry-client.rst @@ -0,0 +1,6 @@ +Foundry Client +=============================================================================================== + +.. automodule:: labelbox.schema.foundry.foundry_client + :members: + :show-inheritance: diff --git a/docs/labelbox/foundry-model.rst b/docs/labelbox/foundry-model.rst new file mode 100644 index 0000000000..52a09bc608 --- /dev/null +++ b/docs/labelbox/foundry-model.rst @@ -0,0 +1,6 @@ +Foundry Model +=============================================================================================== + +.. automodule:: labelbox.schema.foundry.model + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/identifiable.rst b/docs/labelbox/identifiable.rst new file mode 100644 index 0000000000..698cccc1c5 --- /dev/null +++ b/docs/labelbox/identifiable.rst @@ -0,0 +1,6 @@ +Identifiable +=============================================================================================== + +.. automodule:: labelbox.schema.identifiable + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/identifiables.rst b/docs/labelbox/identifiables.rst new file mode 100644 index 0000000000..bc853b1fe8 --- /dev/null +++ b/docs/labelbox/identifiables.rst @@ -0,0 +1,6 @@ +Identifiables +=============================================================================================== + +.. automodule:: labelbox.schema.identifiables + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/index.rst b/docs/labelbox/index.rst new file mode 100644 index 0000000000..348f39cc34 --- /dev/null +++ b/docs/labelbox/index.rst @@ -0,0 +1,44 @@ +Labelbox Python SDK Documentation +=============================================================================================== + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + + annotation-import + app + asset-attachment + batch + benchmark + bulk-import-request + client + conflict-resolution-strategy + datarow + datarow-metadata + dataset + enums + exceptions + export-task + foundry-client + foundry-model + identifiable + identifiables + label + labeling-frontend + labeling-frontend-options + labeling-parameter-override + model + model-run + ontology + organization + pagination + project + quality-mode + resource-tag + review + send-to-annotate-params + slice + task + task-queue + user + webhook \ No newline at end of file diff --git a/docs/labelbox/label.rst b/docs/labelbox/label.rst new file mode 100644 index 0000000000..99606d59ab --- /dev/null +++ b/docs/labelbox/label.rst @@ -0,0 +1,6 @@ +Label +=============================================================================================== + +.. automodule:: labelbox.schema.label + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/labeling-frontend-options.rst b/docs/labelbox/labeling-frontend-options.rst new file mode 100644 index 0000000000..28820696f1 --- /dev/null +++ b/docs/labelbox/labeling-frontend-options.rst @@ -0,0 +1,8 @@ +Labeling Frontend Options +=============================================================================================== + +.. automodule:: labelbox.schema.labeling_frontend + :members: LabelingFrontendOptions + :exclude-members: LabelingFrontend + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/docs/labelbox/labeling-frontend.rst b/docs/labelbox/labeling-frontend.rst new file mode 100644 index 0000000000..874a851793 --- /dev/null +++ b/docs/labelbox/labeling-frontend.rst @@ -0,0 +1,7 @@ +Labeling Frontend +=============================================================================================== + +.. automodule:: labelbox.schema.labeling_frontend + :members: LabelingFrontend + :exclude-members: LabelingFrontendOptions + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/labeling-parameter-override.rst b/docs/labelbox/labeling-parameter-override.rst new file mode 100644 index 0000000000..83a59167ca --- /dev/null +++ b/docs/labelbox/labeling-parameter-override.rst @@ -0,0 +1,7 @@ +Labeling Parameter Override +=============================================================================================== + +.. automodule:: labelbox.schema.project + :members: LabelingParameterOverride + :show-inheritance: + :noindex: \ No newline at end of file diff --git a/docs/labelbox/model-run.rst b/docs/labelbox/model-run.rst new file mode 100644 index 0000000000..12defc3ae7 --- /dev/null +++ b/docs/labelbox/model-run.rst @@ -0,0 +1,6 @@ +Model Run +=============================================================================================== + +.. automodule:: labelbox.schema.model_run + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/model.rst b/docs/labelbox/model.rst new file mode 100644 index 0000000000..05fb5d8e00 --- /dev/null +++ b/docs/labelbox/model.rst @@ -0,0 +1,6 @@ +Model +=============================================================================================== + +.. automodule:: labelbox.schema.model + :members: + :show-inheritance: diff --git a/docs/labelbox/ontology.rst b/docs/labelbox/ontology.rst new file mode 100644 index 0000000000..fba299d75a --- /dev/null +++ b/docs/labelbox/ontology.rst @@ -0,0 +1,7 @@ +Ontology +=============================================================================================== + +.. automodule:: labelbox.schema.ontology + :members: + :exclude-members: OntologyEntity, Classification, Tool, Option + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/organization.rst b/docs/labelbox/organization.rst new file mode 100644 index 0000000000..4b7a1bf0f3 --- /dev/null +++ b/docs/labelbox/organization.rst @@ -0,0 +1,6 @@ +Organization +=============================================================================================== + +.. automodule:: labelbox.schema.organization + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/pagination.rst b/docs/labelbox/pagination.rst new file mode 100644 index 0000000000..9112592fda --- /dev/null +++ b/docs/labelbox/pagination.rst @@ -0,0 +1,7 @@ +Pagination +=============================================================================================== + +.. automodule:: labelbox.pagination + :members: + :special-members: __init__ + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/project.rst b/docs/labelbox/project.rst new file mode 100644 index 0000000000..0baf8b37c0 --- /dev/null +++ b/docs/labelbox/project.rst @@ -0,0 +1,7 @@ +Project +=============================================================================================== + +.. automodule:: labelbox.schema.project + :members: + :exclude-members: LabelerPerformance, LabelingParameterOverride + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/quality-mode.rst b/docs/labelbox/quality-mode.rst new file mode 100644 index 0000000000..d3bc27bcb7 --- /dev/null +++ b/docs/labelbox/quality-mode.rst @@ -0,0 +1,6 @@ +Quality Mode +=============================================================================================== + +.. automodule:: labelbox.schema.quality_mode + :members: + :show-inheritance: diff --git a/docs/labelbox/resource-tag.rst b/docs/labelbox/resource-tag.rst new file mode 100644 index 0000000000..39742f6499 --- /dev/null +++ b/docs/labelbox/resource-tag.rst @@ -0,0 +1,6 @@ +Resource Tag +=============================================================================================== + +.. automodule:: labelbox.schema.resource_tag + :members: + :show-inheritance: diff --git a/docs/labelbox/review.rst b/docs/labelbox/review.rst new file mode 100644 index 0000000000..5d549a7d7c --- /dev/null +++ b/docs/labelbox/review.rst @@ -0,0 +1,6 @@ +Review +=============================================================================================== + +.. automodule:: labelbox.schema.review + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/send-to-annotate-params.rst b/docs/labelbox/send-to-annotate-params.rst new file mode 100644 index 0000000000..946264545c --- /dev/null +++ b/docs/labelbox/send-to-annotate-params.rst @@ -0,0 +1,6 @@ +Send To Annotate Params +=============================================================================================== + +.. automodule:: labelbox.schema.send_to_annotate_params + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/slice.rst b/docs/labelbox/slice.rst new file mode 100644 index 0000000000..cb36060fa3 --- /dev/null +++ b/docs/labelbox/slice.rst @@ -0,0 +1,6 @@ +Slice +=============================================================================================== + +.. automodule:: labelbox.schema.slice + :members: + :show-inheritance: diff --git a/docs/labelbox/task-queue.rst b/docs/labelbox/task-queue.rst new file mode 100644 index 0000000000..87dd309d02 --- /dev/null +++ b/docs/labelbox/task-queue.rst @@ -0,0 +1,6 @@ +Task Queue +=============================================================================================== + +.. automodule:: labelbox.schema.task_queue + :members: + :show-inheritance: diff --git a/docs/labelbox/task.rst b/docs/labelbox/task.rst new file mode 100644 index 0000000000..abc9f48bc8 --- /dev/null +++ b/docs/labelbox/task.rst @@ -0,0 +1,6 @@ +Task +=============================================================================================== + +.. automodule:: labelbox.schema.task + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/user.rst b/docs/labelbox/user.rst new file mode 100644 index 0000000000..44d5eb480f --- /dev/null +++ b/docs/labelbox/user.rst @@ -0,0 +1,6 @@ +User +=============================================================================================== + +.. automodule:: labelbox.schema.user + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/labelbox/webhook.rst b/docs/labelbox/webhook.rst new file mode 100644 index 0000000000..0476663976 --- /dev/null +++ b/docs/labelbox/webhook.rst @@ -0,0 +1,6 @@ +Webhook +=============================================================================================== + +.. automodule:: labelbox.schema.webhook + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 6247f7e231..0000000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 5926fb23da..0000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Sphinx~=5.3.0 -sphinx-rtd-theme==0.5.1 diff --git a/docs/source/_static/js/prevent_collapse.js b/docs/source/_static/js/prevent_collapse.js deleted file mode 100644 index abb7d7fdf1..0000000000 --- a/docs/source/_static/js/prevent_collapse.js +++ /dev/null @@ -1,13 +0,0 @@ - -window.addEventListener('load', (event) => { - var menu = document.querySelector(".wy-menu ul li:first-child") - if (!menu.classList.contains("current")) { - menu.classList.add("current") - } -}); - -window.onload = function() { - if (window.jQuery) { - $(window).off('hashchange') - } -} diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 1dfa1808b5..0000000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,266 +0,0 @@ -Labelbox Python API reference -=================================== - -.. toctree:: - :maxdepth: 3 - :caption: Contents: - -Client ----------------------- - -.. automodule:: labelbox.client - :members: - :special-members: __init__ - :exclude-members: upload_data, upload_file - :show-inheritance: - -AssetAttachment --------------------------------------- - -.. automodule:: labelbox.schema.asset_attachment - :members: - :show-inheritance: - - -Benchmark --------------------------------- - -.. automodule:: labelbox.schema.benchmark - :members: - :show-inheritance: - -BulkImportRequest --------------------------------------------- - -.. automodule:: labelbox.schema.bulk_import_request - :members: - :exclude-members: create_from_local_file, create_from_objects, create_from_url, from_name - :show-inheritance: - -DataRow --------------------------------- - -.. automodule:: labelbox.schema.data_row - :members: - :show-inheritance: - -Dataset ------------------------------- - -.. automodule:: labelbox.schema.dataset - :members: - :show-inheritance: - -Label ----------------------------- - -.. automodule:: labelbox.schema.label - :members: - :show-inheritance: - -LabelingFrontend ------------------------------------------ - -.. automodule:: labelbox.schema.labeling_frontend - :members: LabelingFrontend - :exclude-members: LabelingFrontendOptions - :show-inheritance: - -LabelingFrontendOptions ------------------------------------------ -.. automodule:: labelbox.schema.labeling_frontend - :members: LabelingFrontendOptions - :exclude-members: LabelingFrontend - :show-inheritance: - :noindex: - -LabelingParameterOverride ------------------------------------------ -.. automodule:: labelbox.schema.project - :members: LabelingParameterOverride - :show-inheritance: - :noindex: - -Ontology -------------------------------- - -.. automodule:: labelbox.schema.ontology - :members: - :exclude-members: OntologyEntity, Classification, Tool, Option - :show-inheritance: - -Organization ------------------------------------ - -.. automodule:: labelbox.schema.organization - :members: - :show-inheritance: - -Project ------------------------------- - -.. automodule:: labelbox.schema.project - :members: - :exclude-members: LabelerPerformance, LabelingParameterOverride - :show-inheritance: - -Review ------------------------------ - -.. automodule:: labelbox.schema.review - :members: - :show-inheritance: - -Task ---------------------------- - -.. automodule:: labelbox.schema.task - :members: - :show-inheritance: - -Task Queue ---------------------------- -.. automodule:: labelbox.schema.task_queue - :members: - :show-inheritance: - -User ---------------------------- - -.. automodule:: labelbox.schema.user - :members: - :show-inheritance: - -Webhook ------------------------------- - -.. automodule:: labelbox.schema.webhook - :members: - :show-inheritance: - -Exceptions --------------------------- - -.. automodule:: labelbox.exceptions - :members: - :show-inheritance: - -Pagination --------------------------- - -.. automodule:: labelbox.pagination - :members: - :special-members: __init__ - :show-inheritance: - -Enums ----------------------------- - -.. automodule:: labelbox.schema.enums - :members: - :show-inheritance: - -ModelRun ----------------------------- - -.. automodule:: labelbox.schema.model_run - :members: - :show-inheritance: - -Model ----------------------------- - -.. automodule:: labelbox.schema.model - :members: - :show-inheritance: - -DataRowMetadata ----------------------------- - -.. automodule:: labelbox.schema.data_row_metadata - :members: - :exclude-members: _DeleteBatchDataRowMetadata - :show-inheritance: - -AnnotationImport ----------------------------- - -.. automodule:: labelbox.schema.annotation_import - :members: - :show-inheritance: - -Batch ----------------------------- - -.. automodule:: labelbox.schema.batch - :members: - :show-inheritance: - -ResourceTag ----------------------------- - -.. automodule:: labelbox.schema.resource_tag - :members: - :show-inheritance: - - -Slice ---------------------------- -.. automodule:: labelbox.schema.slice - :members: - :show-inheritance: - -QualityMode ------------------------------------------ -.. automodule:: labelbox.schema.quality_mode - :members: - :show-inheritance: - -ExportTask ---------------------------- -.. automodule:: labelbox.schema.export_task - :members: - :exclude-members: FileRetrieverByLine, FileRetrieverByOffset, FileRetrieverStrategy, Range, Converter - :show-inheritance: - -Identifiables ---------------------------- -.. automodule:: labelbox.schema.identifiables - :members: - :show-inheritance: - -Identifiable ---------------------------- -.. automodule:: labelbox.schema.identifiable - :members: - :show-inheritance: - -ConflictResolutionStrategy ---------------------------- -.. automodule:: labelbox.schema.conflict_resolution_strategy - :members: - :show-inheritance: - -FoundryClient ---------------------------- -.. automodule:: labelbox.schema.foundry.foundry_client - :members: - :show-inheritance: - -App ---------------------------- -.. automodule:: labelbox.schema.foundry.app - :members: - :show-inheritance: - -FoundryModel ---------------------------- -.. automodule:: labelbox.schema.foundry.model - :members: - :show-inheritance: - -SendToAnnotateParams ---------------------------- -.. automodule:: labelbox.schema.send_to_annotate_params - :members: - :show-inheritance: \ No newline at end of file diff --git a/libs/labelbox/pyproject.toml b/libs/labelbox/pyproject.toml index bd786228ae..69e29d6f55 100644 --- a/libs/labelbox/pyproject.toml +++ b/libs/labelbox/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "labelbox" -version = "3.65.0" +version = "3.66.0" description = "Labelbox Python API" authors = [ { name = "Labelbox", email = "engineering@labelbox.com" } @@ -96,5 +96,4 @@ test = { chain = ["lint", "unit", "integration" ] } allow-direct-references = true [tool.hatch.build.targets.wheel] -packages = ["src/labelbox"] - +packages = ["src/labelbox"] \ No newline at end of file diff --git a/libs/labelbox/tests/conftest.py b/libs/labelbox/tests/conftest.py index ac6e08dc06..013d606cff 100644 --- a/libs/labelbox/tests/conftest.py +++ b/libs/labelbox/tests/conftest.py @@ -1,12 +1,54 @@ -import glob from datetime import datetime from random import randint from string import ascii_letters +import json +import os +import re +import uuid +import time +import requests import pytest +from types import SimpleNamespace +from typing import Type +from enum import Enum +from typing import Tuple + +from labelbox import Dataset, DataRow +from labelbox import MediaType +from labelbox.orm import query +from labelbox.pagination import PaginatedCollection +from labelbox.schema.invite import Invite +from labelbox.schema.quality_mode import QualityMode +from labelbox.schema.queue_mode import QueueMode +from labelbox import Client + +from labelbox import Dataset, DataRow +from labelbox import LabelingFrontend +from labelbox import OntologyBuilder, Tool, Option, Classification, MediaType +from labelbox.orm import query +from labelbox.pagination import PaginatedCollection +from labelbox.schema.annotation_import import LabelImport +from labelbox.schema.catalog import Catalog +from labelbox.schema.enums import AnnotationImportState +from labelbox.schema.invite import Invite +from labelbox.schema.quality_mode import QualityMode +from labelbox.schema.queue_mode import QueueMode +from labelbox.schema.user import User +from labelbox import Client + +IMG_URL = "https://picsum.photos/200/300.jpg" +MASKABLE_IMG_URL = "https://storage.googleapis.com/labelbox-datasets/image_sample_data/2560px-Kitano_Street_Kobe01s5s4110.jpeg" +SMALL_DATASET_URL = "https://storage.googleapis.com/lb-artifacts-testing-public/sdk_integration_test/potato.jpeg" +DATA_ROW_PROCESSING_WAIT_TIMEOUT_SECONDS = 30 +DATA_ROW_PROCESSING_WAIT_SLEEP_INTERNAL_SECONDS = 3 +EPHEMERAL_BASE_URL = "http://lb-api-public" +IMAGE_URL = "https://storage.googleapis.com/diagnostics-demo-data/coco/COCO_train2014_000000000034.jpg" +EXTERNAL_ID = "my-image" pytest_plugins = [] + @pytest.fixture(scope="session") def rand_gen(): @@ -23,3 +65,984 @@ def gen(field_type): field_type) return gen + + +class Environ(Enum): + LOCAL = 'local' + PROD = 'prod' + STAGING = 'staging' + CUSTOM = 'custom' + STAGING_EU = 'staging-eu' + EPHEMERAL = 'ephemeral' # Used for testing PRs with ephemeral environments + + +@pytest.fixture +def image_url() -> str: + return IMAGE_URL + + +@pytest.fixture +def external_id() -> str: + return EXTERNAL_ID + + +def ephemeral_endpoint() -> str: + return os.getenv('LABELBOX_TEST_BASE_URL', EPHEMERAL_BASE_URL) + + +def graphql_url(environ: str) -> str: + if environ == Environ.PROD: + return 'https://api.labelbox.com/graphql' + elif environ == Environ.STAGING: + return 'https://api.lb-stage.xyz/graphql' + elif environ == Environ.CUSTOM: + graphql_api_endpoint = os.environ.get( + 'LABELBOX_TEST_GRAPHQL_API_ENDPOINT') + if graphql_api_endpoint is None: + raise Exception("Missing LABELBOX_TEST_GRAPHQL_API_ENDPOINT") + return graphql_api_endpoint + elif environ == Environ.EPHEMERAL: + return f"{ephemeral_endpoint()}/graphql" + return 'http://host.docker.internal:8080/graphql' + + +def rest_url(environ: str) -> str: + if environ == Environ.PROD: + return 'https://api.labelbox.com/api/v1' + elif environ == Environ.STAGING: + return 'https://api.lb-stage.xyz/api/v1' + elif environ == Environ.CUSTOM: + rest_api_endpoint = os.environ.get('LABELBOX_TEST_REST_API_ENDPOINT') + if rest_api_endpoint is None: + raise Exception("Missing LABELBOX_TEST_REST_API_ENDPOINT") + return rest_api_endpoint + elif environ == Environ.EPHEMERAL: + return f"{ephemeral_endpoint()}/api/v1" + return 'http://host.docker.internal:8080/api/v1' + + +def testing_api_key(environ: str) -> str: + for var in [ + "LABELBOX_TEST_API_KEY_PROD", "LABELBOX_TEST_API_KEY_STAGING", + "LABELBOX_TEST_API_KEY_CUSTOM", "LABELBOX_TEST_API_KEY_LOCAL", + "LABELBOX_TEST_API_KEY" + ]: + value = os.environ.get(var) + if value is not None: + return value + raise Exception("Cannot find API to use for tests") + + +def service_api_key() -> str: + service_api_key = os.environ["SERVICE_API_KEY"] + if service_api_key is None: + raise Exception( + "SERVICE_API_KEY is missing and needed for admin client") + return service_api_key + + +class IntegrationClient(Client): + + def __init__(self, environ: str) -> None: + api_url = graphql_url(environ) + api_key = testing_api_key(environ) + rest_endpoint = rest_url(environ) + + super().__init__(api_key, + api_url, + enable_experimental=True, + rest_endpoint=rest_endpoint) + self.queries = [] + + def execute(self, query=None, params=None, check_naming=True, **kwargs): + if check_naming and query is not None: + assert re.match(r"\s*(?:query|mutation) \w+PyApi", + query) is not None + self.queries.append((query, params)) + return super().execute(query, params, **kwargs) + + +class AdminClient(Client): + + def __init__(self, env): + """ + The admin client creates organizations and users using admin api described here https://labelbox.atlassian.net/wiki/spaces/AP/pages/2206564433/Internal+Admin+APIs. + """ + self._api_key = service_api_key() + self._admin_endpoint = f"{ephemeral_endpoint()}/admin/v1" + self._api_url = graphql_url(env) + self._rest_endpoint = rest_url(env) + + super().__init__(self._api_key, + self._api_url, + enable_experimental=True, + rest_endpoint=self._rest_endpoint) + + def _create_organization(self) -> str: + endpoint = f"{self._admin_endpoint}/organizations/" + response = requests.post( + endpoint, + headers=self.headers, + json={"name": f"Test Org {uuid.uuid4()}"}, + ) + + data = response.json() + if response.status_code not in [ + requests.codes.created, requests.codes.ok + ]: + raise Exception("Failed to create org, message: " + + str(data['message'])) + + return data['id'] + + def _create_user(self, organization_id=None) -> Tuple[str, str]: + if organization_id is None: + organization_id = self.organization_id + + endpoint = f"{self._admin_endpoint}/user-identities/" + identity_id = f"e2e+{uuid.uuid4()}" + + response = requests.post( + endpoint, + headers=self.headers, + json={ + "identityId": identity_id, + "email": "email@email.com", + "name": f"tester{uuid.uuid4()}", + "verificationStatus": "VERIFIED", + }, + ) + data = response.json() + if response.status_code not in [ + requests.codes.created, requests.codes.ok + ]: + raise Exception("Failed to create user, message: " + + str(data['message'])) + + user_identity_id = data['identityId'] + + endpoint = f"{self._admin_endpoint}/organizations/{organization_id}/users/" + response = requests.post( + endpoint, + headers=self.headers, + json={ + "identityId": user_identity_id, + "organizationRole": "Admin" + }, + ) + + data = response.json() + if response.status_code not in [ + requests.codes.created, requests.codes.ok + ]: + raise Exception("Failed to create link user to org, message: " + + str(data['message'])) + + user_id = data['id'] + + endpoint = f"{self._admin_endpoint}/users/{user_id}/token" + response = requests.get( + endpoint, + headers=self.headers, + ) + data = response.json() + if response.status_code not in [ + requests.codes.created, requests.codes.ok + ]: + raise Exception("Failed to create ephemeral user, message: " + + str(data['message'])) + + token = data["token"] + + return user_id, token + + def create_api_key_for_user(self) -> str: + organization_id = self._create_organization() + _, user_token = self._create_user(organization_id) + key_name = f"test-key+{uuid.uuid4()}" + query = """mutation CreateApiKeyPyApi($name: String!) { + createApiKey(data: {name: $name}) { + id + jwt + } + } + """ + params = {"name": key_name} + self.headers["Authorization"] = f"Bearer {user_token}" + res = self.execute(query, params, error_log_key="errors") + + return res["createApiKey"]["jwt"] + + +class EphemeralClient(Client): + + def __init__(self, environ=Environ.EPHEMERAL): + self.admin_client = AdminClient(environ) + self.api_key = self.admin_client.create_api_key_for_user() + api_url = graphql_url(environ) + rest_endpoint = rest_url(environ) + + super().__init__(self.api_key, + api_url, + enable_experimental=True, + rest_endpoint=rest_endpoint) + + +@pytest.fixture +def ephmeral_client() -> EphemeralClient: + return EphemeralClient + + +@pytest.fixture +def admin_client() -> AdminClient: + return AdminClient + + +@pytest.fixture +def integration_client() -> IntegrationClient: + return IntegrationClient + + +@pytest.fixture(scope="session") +def environ() -> Environ: + """ + Checks environment variables for LABELBOX_ENVIRON to be + 'prod' or 'staging' + Make sure to set LABELBOX_TEST_ENVIRON in .github/workflows/python-package.yaml + """ + try: + return Environ(os.environ['LABELBOX_TEST_ENVIRON']) + except KeyError: + raise Exception(f'Missing LABELBOX_TEST_ENVIRON in: {os.environ}') + + +def cancel_invite(client, invite_id): + """ + Do not use. Only for testing. + """ + query_str = """mutation CancelInvitePyApi($where: WhereUniqueIdInput!) { + cancelInvite(where: $where) {id}}""" + client.execute(query_str, {'where': {'id': invite_id}}, experimental=True) + + +def get_project_invites(client, project_id): + """ + Do not use. Only for testing. + """ + id_param = "projectId" + query_str = """query GetProjectInvitationsPyApi($from: ID, $first: PageSize, $%s: ID!) { + project(where: {id: $%s}) {id + invites(from: $from, first: $first) { nodes { %s + projectInvites { projectId projectRoleName } } nextCursor}}} + """ % (id_param, id_param, query.results_query_part(Invite)) + return PaginatedCollection(client, + query_str, {id_param: project_id}, + ['project', 'invites', 'nodes'], + Invite, + cursor_path=['project', 'invites', 'nextCursor']) + + +def get_invites(client): + """ + Do not use. Only for testing. + """ + query_str = """query GetOrgInvitationsPyApi($from: ID, $first: PageSize) { + organization { id invites(from: $from, first: $first) { + nodes { id createdAt organizationRoleName inviteeEmail } nextCursor }}}""" + invites = PaginatedCollection( + client, + query_str, {}, ['organization', 'invites', 'nodes'], + Invite, + cursor_path=['organization', 'invites', 'nextCursor'], + experimental=True) + return invites + + +@pytest.fixture +def queries(): + return SimpleNamespace(cancel_invite=cancel_invite, + get_project_invites=get_project_invites, + get_invites=get_invites) + + +@pytest.fixture(scope="session") +def admin_client(environ: str): + return AdminClient(environ) + + +@pytest.fixture(scope="session") +def client(environ: str): + if environ == Environ.EPHEMERAL: + return EphemeralClient() + return IntegrationClient(environ) + + +@pytest.fixture(scope="session") +def image_url(client): + return client.upload_data(requests.get(MASKABLE_IMG_URL).content, + content_type="image/jpeg", + filename="image.jpeg", + sign=True) + + +@pytest.fixture(scope="session") +def pdf_url(client): + pdf_url = client.upload_file('tests/assets/loremipsum.pdf') + return {"row_data": {"pdf_url": pdf_url,}, "global_key": str(uuid.uuid4())} + + +@pytest.fixture(scope="session") +def pdf_entity_data_row(client): + pdf_url = client.upload_file( + 'tests/assets/arxiv-pdf_data_99-word-token-pdfs_0801.3483.pdf') + text_layer_url = client.upload_file( + 'tests/assets/arxiv-pdf_data_99-word-token-pdfs_0801.3483-lb-textlayer.json' + ) + + return { + "row_data": { + "pdf_url": pdf_url, + "text_layer_url": text_layer_url + }, + "global_key": str(uuid.uuid4()) + } + + +@pytest.fixture() +def conversation_entity_data_row(client, rand_gen): + return { + "row_data": + "https://storage.googleapis.com/labelbox-developer-testing-assets/conversational_text/1000-conversations/conversation-1.json", + "global_key": + f"https://storage.googleapis.com/labelbox-developer-testing-assets/conversational_text/1000-conversations/conversation-1.json-{rand_gen(str)}", + } + + +@pytest.fixture +def project(client, rand_gen): + project = client.create_project(name=rand_gen(str), + queue_mode=QueueMode.Batch, + media_type=MediaType.Image) + yield project + project.delete() + + +@pytest.fixture +def consensus_project(client, rand_gen): + project = client.create_project(name=rand_gen(str), + quality_mode=QualityMode.Consensus, + queue_mode=QueueMode.Batch, + media_type=MediaType.Image) + yield project + project.delete() + + +@pytest.fixture +def consensus_project_with_batch(consensus_project, initial_dataset, rand_gen, + image_url): + project = consensus_project + dataset = initial_dataset + + data_rows = [] + for _ in range(3): + data_rows.append({ + DataRow.row_data: image_url, + DataRow.global_key: str(uuid.uuid4()) + }) + task = dataset.create_data_rows(data_rows) + task.wait_till_done() + assert task.status == "COMPLETE" + + data_rows = list(dataset.data_rows()) + assert len(data_rows) == 3 + batch = project.create_batch( + rand_gen(str), + data_rows, # sample of data row objects + 5 # priority between 1(Highest) - 5(lowest) + ) + + yield [project, batch, data_rows] + batch.delete() + + +@pytest.fixture +def dataset(client, rand_gen): + dataset = client.create_dataset(name=rand_gen(str)) + yield dataset + dataset.delete() + + +@pytest.fixture(scope='function') +def unique_dataset(client, rand_gen): + dataset = client.create_dataset(name=rand_gen(str)) + yield dataset + dataset.delete() + + +@pytest.fixture +def small_dataset(dataset: Dataset): + task = dataset.create_data_rows([ + { + "row_data": SMALL_DATASET_URL, + "external_id": "my-image" + }, + ] * 2) + task.wait_till_done() + + yield dataset + + +@pytest.fixture +def data_row(dataset, image_url, rand_gen): + global_key = f"global-key-{rand_gen(str)}" + task = dataset.create_data_rows([ + { + "row_data": image_url, + "external_id": "my-image", + "global_key": global_key + }, + ]) + task.wait_till_done() + dr = dataset.data_rows().get_one() + yield dr + dr.delete() + + +@pytest.fixture +def data_row_and_global_key(dataset, image_url, rand_gen): + global_key = f"global-key-{rand_gen(str)}" + task = dataset.create_data_rows([ + { + "row_data": image_url, + "external_id": "my-image", + "global_key": global_key + }, + ]) + task.wait_till_done() + dr = dataset.data_rows().get_one() + yield dr, global_key + dr.delete() + + +# can be used with +# @pytest.mark.parametrize('data_rows', [], indirect=True) +# if omitted, count defaults to 1 +@pytest.fixture +def data_rows(dataset, image_url, request, wait_for_data_row_processing, + client): + count = 1 + if hasattr(request, 'param'): + count = request.param + + datarows = [ + dict(row_data=image_url, global_key=f"global-key-{uuid.uuid4()}") + for _ in range(count) + ] + + task = dataset.create_data_rows(datarows) + task.wait_till_done() + datarows = dataset.data_rows().get_many(count) + for dr in dataset.data_rows(): + wait_for_data_row_processing(client, dr) + + yield datarows + + for datarow in datarows: + datarow.delete() + + +@pytest.fixture +def iframe_url(environ) -> str: + if environ in [Environ.PROD, Environ.LOCAL]: + return 'https://editor.labelbox.com' + elif environ == Environ.STAGING: + return 'https://editor.lb-stage.xyz' + + +@pytest.fixture +def sample_image() -> str: + path_to_video = 'tests/integration/media/sample_image.jpg' + return path_to_video + + +@pytest.fixture +def sample_video() -> str: + path_to_video = 'tests/integration/media/cat.mp4' + return path_to_video + + +@pytest.fixture +def sample_bulk_conversation() -> list: + path_to_conversation = 'tests/integration/media/bulk_conversation.json' + with open(path_to_conversation) as json_file: + conversations = json.load(json_file) + return conversations + + +@pytest.fixture +def organization(client): + # Must have at least one seat open in your org to run these tests + org = client.get_organization() + # Clean up before and after incase this wasn't run for some reason. + for invite in get_invites(client): + if "@labelbox.com" in invite.email: + cancel_invite(client, invite.uid) + yield org + for invite in get_invites(client): + if "@labelbox.com" in invite.email: + cancel_invite(client, invite.uid) + + +@pytest.fixture +def configured_project_with_label(client, rand_gen, image_url, project, dataset, + data_row, wait_for_label_processing): + """Project with a connected dataset, having one datarow + Project contains an ontology with 1 bbox tool + Additionally includes a create_label method for any needed extra labels + One label is already created and yielded when using fixture + """ + project._wait_until_data_rows_are_processed( + data_row_ids=[data_row.uid], + wait_processing_max_seconds=DATA_ROW_PROCESSING_WAIT_TIMEOUT_SECONDS, + sleep_interval=DATA_ROW_PROCESSING_WAIT_SLEEP_INTERNAL_SECONDS) + + project.create_batch( + rand_gen(str), + [data_row.uid], # sample of data row objects + 5 # priority between 1(Highest) - 5(lowest) + ) + ontology = _setup_ontology(project) + label = _create_label(project, data_row, ontology, + wait_for_label_processing) + yield [project, dataset, data_row, label] + + for label in project.labels(): + label.delete() + + +def _create_label(project, data_row, ontology, wait_for_label_processing): + predictions = [{ + "uuid": str(uuid.uuid4()), + "schemaId": ontology.tools[0].feature_schema_id, + "dataRow": { + "id": data_row.uid + }, + "bbox": { + "top": 20, + "left": 20, + "height": 50, + "width": 50 + } + }] + + def create_label(): + """ Ad-hoc function to create a LabelImport + Creates a LabelImport task which will create a label + """ + upload_task = LabelImport.create_from_objects( + project.client, project.uid, f'label-import-{uuid.uuid4()}', + predictions) + upload_task.wait_until_done(sleep_time_seconds=5) + assert upload_task.state == AnnotationImportState.FINISHED, "Label Import did not finish" + assert len( + upload_task.errors + ) == 0, f"Label Import {upload_task.name} failed with errors {upload_task.errors}" + + project.create_label = create_label + project.create_label() + label = wait_for_label_processing(project)[0] + return label + + +def _setup_ontology(project): + editor = list( + project.client.get_labeling_frontends( + where=LabelingFrontend.name == "editor"))[0] + ontology_builder = OntologyBuilder(tools=[ + Tool(tool=Tool.Type.BBOX, name="test-bbox-class"), + ]) + project.setup(editor, ontology_builder.asdict()) + # TODO: ontology may not be synchronous after setup. remove sleep when api is more consistent + time.sleep(2) + return OntologyBuilder.from_project(project) + + +@pytest.fixture +def big_dataset(dataset: Dataset): + task = dataset.create_data_rows([ + { + "row_data": IMAGE_URL, + "external_id": EXTERNAL_ID + }, + ] * 3) + task.wait_till_done() + + yield dataset + + +@pytest.fixture +def configured_batch_project_with_label(project, dataset, data_row, + wait_for_label_processing): + """Project with a batch having one datarow + Project contains an ontology with 1 bbox tool + Additionally includes a create_label method for any needed extra labels + One label is already created and yielded when using fixture + """ + data_rows = [dr.uid for dr in list(dataset.data_rows())] + project._wait_until_data_rows_are_processed(data_row_ids=data_rows, + sleep_interval=3) + project.create_batch("test-batch", data_rows) + project.data_row_ids = data_rows + + ontology = _setup_ontology(project) + label = _create_label(project, data_row, ontology, + wait_for_label_processing) + + yield [project, dataset, data_row, label] + + for label in project.labels(): + label.delete() + + +@pytest.fixture +def configured_batch_project_with_multiple_datarows(project, dataset, data_rows, + wait_for_label_processing): + """Project with a batch having multiple datarows + Project contains an ontology with 1 bbox tool + Additionally includes a create_label method for any needed extra labels + """ + global_keys = [dr.global_key for dr in data_rows] + + batch_name = f'batch {uuid.uuid4()}' + project.create_batch(batch_name, global_keys=global_keys) + + ontology = _setup_ontology(project) + for datarow in data_rows: + _create_label(project, datarow, ontology, wait_for_label_processing) + + yield [project, dataset, data_rows] + + for label in project.labels(): + label.delete() + + +# NOTE this is nice heuristics, also there is this logic _wait_until_data_rows_are_processed in Project +# in case we still have flakiness in the future, we can use it +@pytest.fixture +def wait_for_data_row_processing(): + """ + Do not use. Only for testing. + + Returns DataRow after waiting for it to finish processing media_attributes. + Some tests, specifically ones that rely on label export, rely on + DataRow be fully processed with media_attributes + """ + + def func(client, data_row, compare_with_prev_media_attrs=False): + """ + added check_updated_at because when a data_row is updated from say + an image to pdf, it already has media_attributes and the loop does + not wait for processing to a pdf + """ + prev_media_attrs = data_row.media_attributes if compare_with_prev_media_attrs else None + data_row_id = data_row.uid + timeout_seconds = 60 + while True: + data_row = client.get_data_row(data_row_id) + if data_row.media_attributes and (prev_media_attrs is None or + prev_media_attrs + != data_row.media_attributes): + return data_row + timeout_seconds -= 2 + if timeout_seconds <= 0: + raise TimeoutError( + f"Timed out waiting for DataRow '{data_row_id}' to finish processing media_attributes" + ) + time.sleep(2) + + return func + + +@pytest.fixture +def wait_for_label_processing(): + """ + Do not use. Only for testing. + + Returns project's labels as a list after waiting for them to finish processing. + If `project.labels()` is called before label is fully processed, + it may return an empty set + """ + + def func(project): + timeout_seconds = 10 + while True: + labels = list(project.labels()) + if len(labels) > 0: + return labels + timeout_seconds -= 2 + if timeout_seconds <= 0: + raise TimeoutError( + f"Timed out waiting for label for project '{project.uid}' to finish processing" + ) + time.sleep(2) + + return func + + +@pytest.fixture +def initial_dataset(client, rand_gen): + dataset = client.create_dataset(name=rand_gen(str)) + yield dataset + + dataset.delete() + + +@pytest.fixture +def video_data(client, rand_gen, video_data_row, wait_for_data_row_processing): + dataset = client.create_dataset(name=rand_gen(str)) + data_row_ids = [] + data_row = dataset.create_data_row(video_data_row) + data_row = wait_for_data_row_processing(client, data_row) + data_row_ids.append(data_row.uid) + yield dataset, data_row_ids + dataset.delete() + + +def create_video_data_row(rand_gen): + return { + "row_data": + "https://storage.googleapis.com/labelbox-datasets/video-sample-data/sample-video-1.mp4", + "global_key": + f"https://storage.googleapis.com/labelbox-datasets/video-sample-data/sample-video-1.mp4-{rand_gen(str)}", + "media_type": + "VIDEO", + } + + +@pytest.fixture +def video_data_100_rows(client, rand_gen, wait_for_data_row_processing): + dataset = client.create_dataset(name=rand_gen(str)) + data_row_ids = [] + for _ in range(100): + data_row = dataset.create_data_row(create_video_data_row(rand_gen)) + data_row = wait_for_data_row_processing(client, data_row) + data_row_ids.append(data_row.uid) + yield dataset, data_row_ids + dataset.delete() + + +@pytest.fixture() +def video_data_row(rand_gen): + return create_video_data_row(rand_gen) + + +class ExportV2Helpers: + + @classmethod + def run_project_export_v2_task(cls, + project, + num_retries=5, + task_name=None, + filters={}, + params={}): + task = None + params = params if params else { + "project_details": True, + "performance_details": False, + "data_row_details": True, + "label_details": True + } + while (num_retries > 0): + task = project.export_v2(task_name=task_name, + filters=filters, + params=params) + task.wait_till_done() + assert task.status == "COMPLETE" + assert task.errors is None + if len(task.result) == 0: + num_retries -= 1 + time.sleep(5) + else: + break + return task.result + + @classmethod + def run_dataset_export_v2_task(cls, + dataset, + num_retries=5, + task_name=None, + filters={}, + params={}): + task = None + params = params if params else { + "performance_details": False, + "label_details": True + } + while (num_retries > 0): + task = dataset.export_v2(task_name=task_name, + filters=filters, + params=params) + task.wait_till_done() + assert task.status == "COMPLETE" + assert task.errors is None + if len(task.result) == 0: + num_retries -= 1 + time.sleep(5) + else: + break + + return task.result + + @classmethod + def run_catalog_export_v2_task(cls, + client, + num_retries=5, + task_name=None, + filters={}, + params={}): + task = None + params = params if params else { + "performance_details": False, + "label_details": True + } + catalog = client.get_catalog() + while (num_retries > 0): + + task = catalog.export_v2(task_name=task_name, + filters=filters, + params=params) + task.wait_till_done() + assert task.status == "COMPLETE" + assert task.errors is None + if len(task.result) == 0: + num_retries -= 1 + time.sleep(5) + else: + break + + return task.result + + +@pytest.fixture +def export_v2_test_helpers() -> Type[ExportV2Helpers]: + return ExportV2Helpers() + + +@pytest.fixture +def big_dataset_data_row_ids(big_dataset: Dataset): + yield [dr.uid for dr in list(big_dataset.export_data_rows())] + + +@pytest.fixture(scope='function') +def dataset_with_invalid_data_rows(unique_dataset: Dataset, + upload_invalid_data_rows_for_dataset): + upload_invalid_data_rows_for_dataset(unique_dataset) + + yield unique_dataset + + +@pytest.fixture +def upload_invalid_data_rows_for_dataset(): + + def _upload_invalid_data_rows_for_dataset(dataset: Dataset): + task = dataset.create_data_rows([ + { + "row_data": 'gs://invalid-bucket/example.png', # forbidden + "external_id": "image-without-access.jpg" + }, + ] * 2) + task.wait_till_done() + + return _upload_invalid_data_rows_for_dataset + + +@pytest.fixture +def configured_project(project_with_empty_ontology, initial_dataset, rand_gen, + image_url): + dataset = initial_dataset + data_row_id = dataset.create_data_row(row_data=image_url).uid + project = project_with_empty_ontology + + batch = project.create_batch( + rand_gen(str), + [data_row_id], # sample of data row objects + 5 # priority between 1(Highest) - 5(lowest) + ) + project.data_row_ids = [data_row_id] + + yield project + + batch.delete() + + +@pytest.fixture +def project_with_empty_ontology(project): + editor = list( + project.client.get_labeling_frontends( + where=LabelingFrontend.name == "editor"))[0] + empty_ontology = {"tools": [], "classifications": []} + project.setup(editor, empty_ontology) + yield project + + +@pytest.fixture +def configured_project_with_complex_ontology(client, initial_dataset, rand_gen, + image_url): + project = client.create_project(name=rand_gen(str), + queue_mode=QueueMode.Batch, + media_type=MediaType.Image) + dataset = initial_dataset + data_row = dataset.create_data_row(row_data=image_url) + data_row_ids = [data_row.uid] + + project.create_batch( + rand_gen(str), + data_row_ids, # sample of data row objects + 5 # priority between 1(Highest) - 5(lowest) + ) + project.data_row_ids = data_row_ids + + editor = list( + project.client.get_labeling_frontends( + where=LabelingFrontend.name == "editor"))[0] + + ontology = OntologyBuilder() + tools = [ + Tool(tool=Tool.Type.BBOX, name="test-bbox-class"), + Tool(tool=Tool.Type.LINE, name="test-line-class"), + Tool(tool=Tool.Type.POINT, name="test-point-class"), + Tool(tool=Tool.Type.POLYGON, name="test-polygon-class"), + Tool(tool=Tool.Type.NER, name="test-ner-class") + ] + + options = [ + Option(value="first option answer"), + Option(value="second option answer"), + Option(value="third option answer") + ] + + classifications = [ + Classification(class_type=Classification.Type.TEXT, + name="test-text-class"), + Classification(class_type=Classification.Type.DROPDOWN, + name="test-dropdown-class", + options=options), + Classification(class_type=Classification.Type.RADIO, + name="test-radio-class", + options=options), + Classification(class_type=Classification.Type.CHECKLIST, + name="test-checklist-class", + options=options) + ] + + for t in tools: + for c in classifications: + t.add_classification(c) + ontology.add_tool(t) + for c in classifications: + ontology.add_classification(c) + + project.setup(editor, ontology.asdict()) + + yield [project, data_row] + project.delete() diff --git a/libs/labelbox/tests/data/annotation_import/conftest.py b/libs/labelbox/tests/data/annotation_import/conftest.py index e372b1bc9e..02219521d4 100644 --- a/libs/labelbox/tests/data/annotation_import/conftest.py +++ b/libs/labelbox/tests/data/annotation_import/conftest.py @@ -19,34 +19,26 @@ @pytest.fixture() def audio_data_row(rand_gen): return { - "row_data": - "https://storage.googleapis.com/labelbox-datasets/audio-sample-data/sample-audio-1.mp3", - "global_key": - f"https://storage.googleapis.com/labelbox-datasets/audio-sample-data/sample-audio-1.mp3-{rand_gen(str)}", - "media_type": - "AUDIO", + "row_data": "https://storage.googleapis.com/labelbox-datasets/audio-sample-data/sample-audio-1.mp3", + "global_key": f"https://storage.googleapis.com/labelbox-datasets/audio-sample-data/sample-audio-1.mp3-{rand_gen(str)}", + "media_type": "AUDIO", } @pytest.fixture() def conversation_data_row(rand_gen): return { - "row_data": - "https://storage.googleapis.com/labelbox-developer-testing-assets/conversational_text/1000-conversations/conversation-1.json", - "global_key": - f"https://storage.googleapis.com/labelbox-developer-testing-assets/conversational_text/1000-conversations/conversation-1.json-{rand_gen(str)}", + "row_data": "https://storage.googleapis.com/labelbox-developer-testing-assets/conversational_text/1000-conversations/conversation-1.json", + "global_key": f"https://storage.googleapis.com/labelbox-developer-testing-assets/conversational_text/1000-conversations/conversation-1.json-{rand_gen(str)}", } @pytest.fixture() def dicom_data_row(rand_gen): return { - "row_data": - "https://storage.googleapis.com/labelbox-datasets/dicom-sample-data/sample-dicom-1.dcm", - "global_key": - f"https://storage.googleapis.com/labelbox-datasets/dicom-sample-data/sample-dicom-1.dcm-{rand_gen(str)}", - "media_type": - "DICOM", + "row_data": "https://storage.googleapis.com/labelbox-datasets/dicom-sample-data/sample-dicom-1.dcm", + "global_key": f"https://storage.googleapis.com/labelbox-datasets/dicom-sample-data/sample-dicom-1.dcm-{rand_gen(str)}", + "media_type": "DICOM", } @@ -54,43 +46,34 @@ def dicom_data_row(rand_gen): def geospatial_data_row(rand_gen): return { "row_data": { - "tile_layer_url": - "https://s3-us-west-1.amazonaws.com/lb-tiler-layers/mexico_city/{z}/{x}/{y}.png", - "bounds": [[19.405662413477728, -99.21052827588443], - [19.400498983095076, -99.20534818927473]], - "min_zoom": - 12, - "max_zoom": - 20, - "epsg": - "EPSG4326", + "tile_layer_url": "https://s3-us-west-1.amazonaws.com/lb-tiler-layers/mexico_city/{z}/{x}/{y}.png", + "bounds": [ + [19.405662413477728, -99.21052827588443], + [19.400498983095076, -99.20534818927473], + ], + "min_zoom": 12, + "max_zoom": 20, + "epsg": "EPSG4326", }, - "global_key": - f"https://s3-us-west-1.amazonaws.com/lb-tiler-layers/mexico_city/z/x/y.png-{rand_gen(str)}", - "media_type": - "TMS_GEO", + "global_key": f"https://s3-us-west-1.amazonaws.com/lb-tiler-layers/mexico_city/z/x/y.png-{rand_gen(str)}", + "media_type": "TMS_GEO", } @pytest.fixture() def html_data_row(rand_gen): return { - "row_data": - "https://storage.googleapis.com/labelbox-datasets/html_sample_data/sample_html_1.html", - "global_key": - f"https://storage.googleapis.com/labelbox-datasets/html_sample_data/sample_html_1.html-{rand_gen(str)}", + "row_data": "https://storage.googleapis.com/labelbox-datasets/html_sample_data/sample_html_1.html", + "global_key": f"https://storage.googleapis.com/labelbox-datasets/html_sample_data/sample_html_1.html-{rand_gen(str)}", } @pytest.fixture() def image_data_row(rand_gen): return { - "row_data": - "https://lb-test-data.s3.us-west-1.amazonaws.com/image-samples/sample-image-1.jpg", - "global_key": - f"https://lb-test-data.s3.us-west-1.amazonaws.com/image-samples/sample-image-1.jpg-{rand_gen(str)}", - "media_type": - "IMAGE", + "row_data": "https://lb-test-data.s3.us-west-1.amazonaws.com/image-samples/sample-image-1.jpg", + "global_key": f"https://lb-test-data.s3.us-west-1.amazonaws.com/image-samples/sample-image-1.jpg-{rand_gen(str)}", + "media_type": "IMAGE", } @@ -98,391 +81,375 @@ def image_data_row(rand_gen): def document_data_row(rand_gen): return { "row_data": { - "pdf_url": - "https://storage.googleapis.com/labelbox-datasets/arxiv-pdf/data/99-word-token-pdfs/0801.3483.pdf", - "text_layer_url": - "https://storage.googleapis.com/labelbox-datasets/arxiv-pdf/data/99-word-token-pdfs/0801.3483-lb-textlayer.json" + "pdf_url": "https://storage.googleapis.com/labelbox-datasets/arxiv-pdf/data/99-word-token-pdfs/0801.3483.pdf", + "text_layer_url": "https://storage.googleapis.com/labelbox-datasets/arxiv-pdf/data/99-word-token-pdfs/0801.3483-lb-textlayer.json", }, - "global_key": - f"https://storage.googleapis.com/labelbox-datasets/arxiv-pdf/data/99-word-token-pdfs/0801.3483.pdf-{rand_gen(str)}", - "media_type": - "PDF", + "global_key": f"https://storage.googleapis.com/labelbox-datasets/arxiv-pdf/data/99-word-token-pdfs/0801.3483.pdf-{rand_gen(str)}", + "media_type": "PDF", } @pytest.fixture() def text_data_row(rand_gen): return { - "row_data": - "https://storage.googleapis.com/lb-artifacts-testing-public/sdk_integration_test/sample-text-1.txt", - "global_key": - f"https://storage.googleapis.com/lb-artifacts-testing-public/sdk_integration_test/sample-text-1.txt-{rand_gen(str)}", - "media_type": - "TEXT", + "row_data": "https://storage.googleapis.com/lb-artifacts-testing-public/sdk_integration_test/sample-text-1.txt", + "global_key": f"https://storage.googleapis.com/lb-artifacts-testing-public/sdk_integration_test/sample-text-1.txt-{rand_gen(str)}", + "media_type": "TEXT", } @pytest.fixture() def llm_prompt_creation_data_row(rand_gen): return { - "row_data": { - "type": "application/llm.prompt-creation", - "version": 1 - }, - "global_key": rand_gen(str) + "row_data": {"type": "application/llm.prompt-creation", "version": 1}, + "global_key": rand_gen(str), } @pytest.fixture() def llm_prompt_response_data_row(rand_gen): return { - "row_data": { - "type": "application/llm.prompt-response-creation", - "version": 1 - }, - "global_key": rand_gen(str) + "row_data": {"type": "application/llm.prompt-response-creation", "version": 1}, + "global_key": rand_gen(str), } @pytest.fixture -def data_row_json_by_data_type(audio_data_row, conversation_data_row, - dicom_data_row, geospatial_data_row, - html_data_row, image_data_row, document_data_row, - text_data_row, video_data_row, - llm_prompt_creation_data_row, - llm_prompt_response_data_row): +def data_row_json_by_data_type( + audio_data_row, + conversation_data_row, + dicom_data_row, + geospatial_data_row, + html_data_row, + image_data_row, + document_data_row, + text_data_row, + video_data_row, + llm_prompt_creation_data_row, + llm_prompt_response_data_row, +): return { - 'audio': audio_data_row, - 'conversation': conversation_data_row, - 'dicom': dicom_data_row, - 'geospatial': geospatial_data_row, - 'html': html_data_row, - 'image': image_data_row, - 'document': document_data_row, - 'text': text_data_row, - 'video': video_data_row, - 'llmpromptcreation': llm_prompt_creation_data_row, - 'llmpromptresponsecreation': llm_prompt_response_data_row, - 'llmresponsecreation': text_data_row + "audio": audio_data_row, + "conversation": conversation_data_row, + "dicom": dicom_data_row, + "geospatial": geospatial_data_row, + "html": html_data_row, + "image": image_data_row, + "document": document_data_row, + "text": text_data_row, + "video": video_data_row, + "llmpromptcreation": llm_prompt_creation_data_row, + "llmpromptresponsecreation": llm_prompt_response_data_row, + "llmresponsecreation": text_data_row, } @pytest.fixture -def exports_v2_by_data_type(expected_export_v2_image, expected_export_v2_audio, - expected_export_v2_html, expected_export_v2_text, - expected_export_v2_video, - expected_export_v2_conversation, - expected_export_v2_dicom, - expected_export_v2_document, - expected_export_v2_llm_prompt_creation, - expected_export_v2_llm_prompt_response_creation, - expected_export_v2_llm_response_creation): +def exports_v2_by_data_type( + expected_export_v2_image, + expected_export_v2_audio, + expected_export_v2_html, + expected_export_v2_text, + expected_export_v2_video, + expected_export_v2_conversation, + expected_export_v2_dicom, + expected_export_v2_document, + expected_export_v2_llm_prompt_creation, + expected_export_v2_llm_prompt_response_creation, + expected_export_v2_llm_response_creation, +): return { - 'image': - expected_export_v2_image, - 'audio': - expected_export_v2_audio, - 'html': - expected_export_v2_html, - 'text': - expected_export_v2_text, - 'video': - expected_export_v2_video, - 'conversation': - expected_export_v2_conversation, - 'dicom': - expected_export_v2_dicom, - 'document': - expected_export_v2_document, - 'llmpromptcreation': - expected_export_v2_llm_prompt_creation, - 'llmpromptresponsecreation': - expected_export_v2_llm_prompt_response_creation, - 'llmresponsecreation': - expected_export_v2_llm_response_creation + "image": expected_export_v2_image, + "audio": expected_export_v2_audio, + "html": expected_export_v2_html, + "text": expected_export_v2_text, + "video": expected_export_v2_video, + "conversation": expected_export_v2_conversation, + "dicom": expected_export_v2_dicom, + "document": expected_export_v2_document, + "llmpromptcreation": expected_export_v2_llm_prompt_creation, + "llmpromptresponsecreation": expected_export_v2_llm_prompt_response_creation, + "llmresponsecreation": expected_export_v2_llm_response_creation, } @pytest.fixture -def annotations_by_data_type(polygon_inference, rectangle_inference, - rectangle_inference_document, line_inference, - entity_inference, entity_inference_document, - checklist_inference, text_inference, - video_checklist_inference): +def annotations_by_data_type( + polygon_inference, + rectangle_inference, + rectangle_inference_document, + line_inference, + entity_inference, + entity_inference_document, + checklist_inference, + text_inference, + video_checklist_inference, +): return { - 'audio': [checklist_inference, text_inference], - 'conversation': [checklist_inference, text_inference, entity_inference], - 'dicom': [line_inference], - 'document': [ - entity_inference_document, checklist_inference, text_inference, - rectangle_inference_document + "audio": [checklist_inference, text_inference], + "conversation": [checklist_inference, text_inference, entity_inference], + "dicom": [line_inference], + "document": [ + entity_inference_document, + checklist_inference, + text_inference, + rectangle_inference_document, ], - 'html': [text_inference, checklist_inference], - 'image': [ - polygon_inference, rectangle_inference, line_inference, - checklist_inference, text_inference + "html": [text_inference, checklist_inference], + "image": [ + polygon_inference, + rectangle_inference, + line_inference, + checklist_inference, + text_inference, ], - 'text': [entity_inference, checklist_inference, text_inference], - 'video': [video_checklist_inference], - 'llmpromptcreation': [checklist_inference, text_inference], - 'llmpromptresponsecreation': [checklist_inference, text_inference], - 'llmresponsecreation': [checklist_inference, text_inference] + "text": [entity_inference, checklist_inference, text_inference], + "video": [video_checklist_inference], + "llmpromptcreation": [checklist_inference, text_inference], + "llmpromptresponsecreation": [checklist_inference, text_inference], + "llmresponsecreation": [checklist_inference, text_inference], } @pytest.fixture def annotations_by_data_type_v2( - polygon_inference, rectangle_inference, rectangle_inference_document, - line_inference_v2, line_inference, entity_inference, - entity_inference_index, entity_inference_document, - checklist_inference_index, text_inference_index, checklist_inference, - text_inference, video_checklist_inference): + polygon_inference, + rectangle_inference, + rectangle_inference_document, + line_inference_v2, + line_inference, + entity_inference, + entity_inference_index, + entity_inference_document, + checklist_inference_index, + text_inference_index, + checklist_inference, + text_inference, + video_checklist_inference, +): return { - 'audio': [checklist_inference, text_inference], - 'conversation': [ - checklist_inference_index, text_inference_index, - entity_inference_index + "audio": [checklist_inference, text_inference], + "conversation": [ + checklist_inference_index, + text_inference_index, + entity_inference_index, ], - 'dicom': [line_inference_v2], - 'document': [ - entity_inference_document, checklist_inference, text_inference, - rectangle_inference_document + "dicom": [line_inference_v2], + "document": [ + entity_inference_document, + checklist_inference, + text_inference, + rectangle_inference_document, ], - 'html': [text_inference, checklist_inference], - 'image': [ - polygon_inference, rectangle_inference, line_inference, - checklist_inference, text_inference + "html": [text_inference, checklist_inference], + "image": [ + polygon_inference, + rectangle_inference, + line_inference, + checklist_inference, + text_inference, ], - 'text': [entity_inference, checklist_inference, text_inference], - 'video': [video_checklist_inference], - 'llmpromptcreation': [checklist_inference, text_inference], - 'llmpromptresponsecreation': [checklist_inference, text_inference], - 'llmresponsecreation': [checklist_inference, text_inference] + "text": [entity_inference, checklist_inference, text_inference], + "video": [video_checklist_inference], + "llmpromptcreation": [checklist_inference, text_inference], + "llmpromptresponsecreation": [checklist_inference, text_inference], + "llmresponsecreation": [checklist_inference, text_inference], } -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def ontology(): bbox_tool_with_nested_text = { - 'required': - False, - 'name': - 'bbox_tool_with_nested_text', - 'tool': - 'rectangle', - 'color': - '#a23030', - 'classifications': [{ - 'required': - False, - 'instructions': - 'nested', - 'name': - 'nested', - 'type': - 'radio', - 'options': [{ - 'label': - 'radio_option_1', - 'value': - 'radio_value_1', - 'options': [{ - 'required': - False, - 'instructions': - 'nested_checkbox', - 'name': - 'nested_checkbox', - 'type': - 'checklist', - 'options': [{ - 'label': 'nested_checkbox_option_1', - 'value': 'nested_checkbox_value_1', - 'options': [] - }, { - 'label': 'nested_checkbox_option_2', - 'value': 'nested_checkbox_value_2' - }] - }, { - 'required': False, - 'instructions': 'nested_text', - 'name': 'nested_text', - 'type': 'text', - 'options': [] - }] - },] - }] + "required": False, + "name": "bbox_tool_with_nested_text", + "tool": "rectangle", + "color": "#a23030", + "classifications": [ + { + "required": False, + "instructions": "nested", + "name": "nested", + "type": "radio", + "options": [ + { + "label": "radio_option_1", + "value": "radio_value_1", + "options": [ + { + "required": False, + "instructions": "nested_checkbox", + "name": "nested_checkbox", + "type": "checklist", + "options": [ + { + "label": "nested_checkbox_option_1", + "value": "nested_checkbox_value_1", + "options": [], + }, + { + "label": "nested_checkbox_option_2", + "value": "nested_checkbox_value_2", + }, + ], + }, + { + "required": False, + "instructions": "nested_text", + "name": "nested_text", + "type": "text", + "options": [], + }, + ], + }, + ], + } + ], } bbox_tool = { - 'required': - False, - 'name': - 'bbox', - 'tool': - 'rectangle', - 'color': - '#a23030', - 'classifications': [{ - 'required': - False, - 'instructions': - 'nested', - 'name': - 'nested', - 'type': - 'radio', - 'options': [{ - 'label': - 'radio_option_1', - 'value': - 'radio_value_1', - 'options': [{ - 'required': - False, - 'instructions': - 'nested_checkbox', - 'name': - 'nested_checkbox', - 'type': - 'checklist', - 'options': [{ - 'label': 'nested_checkbox_option_1', - 'value': 'nested_checkbox_value_1', - 'options': [] - }, { - 'label': 'nested_checkbox_option_2', - 'value': 'nested_checkbox_value_2' - }] - }] - },] - }] + "required": False, + "name": "bbox", + "tool": "rectangle", + "color": "#a23030", + "classifications": [ + { + "required": False, + "instructions": "nested", + "name": "nested", + "type": "radio", + "options": [ + { + "label": "radio_option_1", + "value": "radio_value_1", + "options": [ + { + "required": False, + "instructions": "nested_checkbox", + "name": "nested_checkbox", + "type": "checklist", + "options": [ + { + "label": "nested_checkbox_option_1", + "value": "nested_checkbox_value_1", + "options": [], + }, + { + "label": "nested_checkbox_option_2", + "value": "nested_checkbox_value_2", + }, + ], + } + ], + }, + ], + } + ], } polygon_tool = { - 'required': False, - 'name': 'polygon', - 'tool': 'polygon', - 'color': '#FF34FF', - 'classifications': [] + "required": False, + "name": "polygon", + "tool": "polygon", + "color": "#FF34FF", + "classifications": [], } polyline_tool = { - 'required': False, - 'name': 'polyline', - 'tool': 'line', - 'color': '#FF4A46', - 'classifications': [] + "required": False, + "name": "polyline", + "tool": "line", + "color": "#FF4A46", + "classifications": [], } point_tool = { - 'required': False, - 'name': 'point--', - 'tool': 'point', - 'color': '#008941', - 'classifications': [] + "required": False, + "name": "point--", + "tool": "point", + "color": "#008941", + "classifications": [], } entity_tool = { - 'required': False, - 'name': 'entity--', - 'tool': 'named-entity', - 'color': '#006FA6', - 'classifications': [] + "required": False, + "name": "entity--", + "tool": "named-entity", + "color": "#006FA6", + "classifications": [], } segmentation_tool = { - 'required': False, - 'name': 'segmentation--', - 'tool': 'superpixel', - 'color': '#A30059', - 'classifications': [] + "required": False, + "name": "segmentation--", + "tool": "superpixel", + "color": "#A30059", + "classifications": [], } raster_segmentation_tool = { - 'required': False, - 'name': 'segmentation_mask', - 'tool': 'raster-segmentation', - 'color': '#ff0000', - 'classifications': [] + "required": False, + "name": "segmentation_mask", + "tool": "raster-segmentation", + "color": "#ff0000", + "classifications": [], } checklist = { - 'required': - False, - 'instructions': - 'checklist', - 'name': - 'checklist', - 'type': - 'checklist', - 'options': [{ - 'label': 'option1', - 'value': 'option1' - }, { - 'label': 'option2', - 'value': 'option2' - }, { - 'label': 'optionN', - 'value': 'optionn' - }] + "required": False, + "instructions": "checklist", + "name": "checklist", + "type": "checklist", + "options": [ + {"label": "option1", "value": "option1"}, + {"label": "option2", "value": "option2"}, + {"label": "optionN", "value": "optionn"}, + ], } checklist_index = { - 'required': - False, - 'instructions': - 'checklist_index', - 'name': - 'checklist_index', - 'type': - 'checklist', - 'scope': - 'index', - 'options': [{ - 'label': 'option1_index', - 'value': 'option1_index' - }, { - 'label': 'option2_index', - 'value': 'option2_index' - }, { - 'label': 'optionN_index', - 'value': 'optionn_index' - }] + "required": False, + "instructions": "checklist_index", + "name": "checklist_index", + "type": "checklist", + "scope": "index", + "options": [ + {"label": "option1_index", "value": "option1_index"}, + {"label": "option2_index", "value": "option2_index"}, + {"label": "optionN_index", "value": "optionn_index"}, + ], } free_form_text = { - 'required': False, - 'instructions': 'text', - 'name': 'text', - 'type': 'text', - 'options': [] + "required": False, + "instructions": "text", + "name": "text", + "type": "text", + "options": [], } free_form_text_index = { - 'required': False, - 'instructions': 'text_index', - 'name': 'text_index', - 'type': 'text', - 'scope': 'index', - 'options': [] + "required": False, + "instructions": "text_index", + "name": "text_index", + "type": "text", + "scope": "index", + "options": [], } radio = { - 'required': - False, - 'instructions': - 'radio', - 'name': - 'radio', - 'type': - 'radio', - 'options': [{ - 'label': 'first_radio_answer', - 'value': 'first_radio_answer', - 'options': [] - }, { - 'label': 'second_radio_answer', - 'value': 'second_radio_answer', - 'options': [] - }] + "required": False, + "instructions": "radio", + "name": "radio", + "type": "radio", + "options": [ + { + "label": "first_radio_answer", + "value": "first_radio_answer", + "options": [], + }, + { + "label": "second_radio_answer", + "value": "second_radio_answer", + "options": [], + }, + ], } named_entity = { - 'tool': 'named-entity', - 'name': 'named-entity', - 'required': False, - 'color': '#A30059', - 'classifications': [], + "tool": "named-entity", + "name": "named-entity", + "required": False, + "color": "#A30059", + "classifications": [], } tools = [ @@ -497,7 +464,11 @@ def ontology(): named_entity, ] classifications = [ - checklist, checklist_index, free_form_text, free_form_text_index, radio + checklist, + checklist_index, + free_form_text, + free_form_text_index, + radio, ] return {"tools": tools, "classifications": classifications} @@ -530,7 +501,6 @@ def func(project): @pytest.fixture def configured_project_datarow_id(configured_project): - def get_data_row_id(indx=0): return configured_project.data_row_ids[indx] @@ -539,7 +509,6 @@ def get_data_row_id(indx=0): @pytest.fixture def configured_project_one_datarow_id(configured_project_with_one_data_row): - def get_data_row_id(indx=0): return configured_project_with_one_data_row.data_row_ids[0] @@ -549,28 +518,30 @@ def get_data_row_id(indx=0): @pytest.fixture def configured_project(client, initial_dataset, ontology, rand_gen, image_url): dataset = initial_dataset - project = client.create_project(name=rand_gen(str), - queue_mode=QueueMode.Batch) + project = client.create_project(name=rand_gen(str), queue_mode=QueueMode.Batch) editor = list( - client.get_labeling_frontends( - where=LabelingFrontend.name == "editor"))[0] + client.get_labeling_frontends(where=LabelingFrontend.name == "editor") + )[0] project.setup(editor, ontology) data_row_ids = [] - ontologies = ontology['tools'] + ontology['classifications'] + ontologies = ontology["tools"] + ontology["classifications"] for ind in range(len(ontologies)): data_row_ids.append( dataset.create_data_row( row_data=image_url, - global_key=f"gk_{ontologies[ind]['name']}_{rand_gen(str)}").uid) - project._wait_until_data_rows_are_processed(data_row_ids=data_row_ids, - sleep_interval=3) + global_key=f"gk_{ontologies[ind]['name']}_{rand_gen(str)}", + ).uid + ) + project._wait_until_data_rows_are_processed( + data_row_ids=data_row_ids, sleep_interval=3 + ) project.create_batch( rand_gen(str), data_row_ids, # sample of data row objects - 5 # priority between 1(Highest) - 5(lowest) + 5, # priority between 1(Highest) - 5(lowest) ) project.data_row_ids = data_row_ids @@ -581,12 +552,12 @@ def configured_project(client, initial_dataset, ontology, rand_gen, image_url): @pytest.fixture def project_with_ontology(client, configured_project, ontology, rand_gen): - project = client.create_project(name=rand_gen(str), - queue_mode=QueueMode.Batch, - media_type=MediaType.Image) + project = client.create_project( + name=rand_gen(str), queue_mode=QueueMode.Batch, media_type=MediaType.Image + ) editor = list( - client.get_labeling_frontends( - where=LabelingFrontend.name == "editor"))[0] + client.get_labeling_frontends(where=LabelingFrontend.name == "editor") + )[0] project.setup(editor, ontology) yield project, ontology @@ -596,20 +567,20 @@ def project_with_ontology(client, configured_project, ontology, rand_gen): @pytest.fixture def configured_project_pdf(client, ontology, rand_gen, pdf_url): - project = client.create_project(name=rand_gen(str), - queue_mode=QueueMode.Batch, - media_type=MediaType.Pdf) + project = client.create_project( + name=rand_gen(str), queue_mode=QueueMode.Batch, media_type=MediaType.Pdf + ) dataset = client.create_dataset(name=rand_gen(str)) editor = list( - client.get_labeling_frontends( - where=LabelingFrontend.name == "editor"))[0] + client.get_labeling_frontends(where=LabelingFrontend.name == "editor") + )[0] project.setup(editor, ontology) data_row = dataset.create_data_row(pdf_url) data_row_ids = [data_row.uid] project.create_batch( rand_gen(str), data_row_ids, # sample of data row objects - 5 # priority between 1(Highest) - 5(lowest) + 5, # priority between 1(Highest) - 5(lowest) ) project.data_row_ids = data_row_ids yield project @@ -628,8 +599,9 @@ def dataset_pdf_entity(client, rand_gen, document_data_row): @pytest.fixture -def dataset_conversation_entity(client, rand_gen, conversation_entity_data_row, - wait_for_data_row_processing): +def dataset_conversation_entity( + client, rand_gen, conversation_entity_data_row, wait_for_data_row_processing +): dataset = client.create_dataset(name=rand_gen(str)) data_row_ids = [] data_row = dataset.create_data_row(conversation_entity_data_row) @@ -641,25 +613,27 @@ def dataset_conversation_entity(client, rand_gen, conversation_entity_data_row, @pytest.fixture -def configured_project_with_one_data_row(client, ontology, rand_gen, - initial_dataset, image_url): - project = client.create_project(name=rand_gen(str), - description=rand_gen(str), - queue_mode=QueueMode.Batch) +def configured_project_with_one_data_row( + client, ontology, rand_gen, initial_dataset, image_url +): + project = client.create_project( + name=rand_gen(str), description=rand_gen(str), queue_mode=QueueMode.Batch + ) editor = list( - client.get_labeling_frontends( - where=LabelingFrontend.name == "editor"))[0] + client.get_labeling_frontends(where=LabelingFrontend.name == "editor") + )[0] project.setup(editor, ontology) data_row = initial_dataset.create_data_row(row_data=image_url) data_row_ids = [data_row.uid] - project._wait_until_data_rows_are_processed(data_row_ids=data_row_ids, - sleep_interval=3) + project._wait_until_data_rows_are_processed( + data_row_ids=data_row_ids, sleep_interval=3 + ) batch = project.create_batch( rand_gen(str), data_row_ids, # sample of data row objects - 5 # priority between 1(Highest) - 5(lowest) + 5, # priority between 1(Highest) - 5(lowest) ) project.data_row_ids = data_row_ids @@ -694,40 +668,42 @@ def configured_project_with_one_data_row(client, ontology, rand_gen, @pytest.fixture def prediction_id_mapping(ontology, request): # Maps tool types to feature schema ids - if 'configured_project' in request.fixturenames: - data_row_id_factory = request.getfixturevalue( - 'configured_project_datarow_id') - project = request.getfixturevalue('configured_project') - elif 'hardcoded_datarow_id' in request.fixturenames: - data_row_id_factory = request.getfixturevalue('hardcoded_datarow_id') - project = request.getfixturevalue('configured_project_with_ontology') + if "configured_project" in request.fixturenames: + data_row_id_factory = request.getfixturevalue("configured_project_datarow_id") + project = request.getfixturevalue("configured_project") + elif "hardcoded_datarow_id" in request.fixturenames: + data_row_id_factory = request.getfixturevalue("hardcoded_datarow_id") + project = request.getfixturevalue("configured_project_with_ontology") else: data_row_id_factory = request.getfixturevalue( - 'configured_project_one_datarow_id') - project = request.getfixturevalue( - 'configured_project_with_one_data_row') + "configured_project_one_datarow_id" + ) + project = request.getfixturevalue("configured_project_with_one_data_row") ontology = project.ontology().normalized result = {} - for idx, tool in enumerate(ontology['tools'] + ontology['classifications']): - if 'tool' in tool: - tool_type = tool['tool'] + for idx, tool in enumerate(ontology["tools"] + ontology["classifications"]): + if "tool" in tool: + tool_type = tool["tool"] else: - tool_type = tool[ - 'type'] if 'scope' not in tool else f"{tool['type']}_{tool['scope']}" # so 'checklist' of 'checklist_index' + tool_type = ( + tool["type"] + if "scope" not in tool + else f"{tool['type']}_{tool['scope']}" + ) # so 'checklist' of 'checklist_index' # TODO: remove this once we have a better way to associate multiple tools instances with a single tool type - if tool_type == 'rectangle': + if tool_type == "rectangle": value = { "uuid": str(uuid.uuid4()), - "schemaId": tool['featureSchemaId'], - "name": tool['name'], + "schemaId": tool["featureSchemaId"], + "name": tool["name"], "dataRow": { "id": data_row_id_factory(idx), }, - 'tool': tool + "tool": tool, } if tool_type not in result: result[tool_type] = [] @@ -735,124 +711,114 @@ def prediction_id_mapping(ontology, request): else: result[tool_type] = { "uuid": str(uuid.uuid4()), - "schemaId": tool['featureSchemaId'], - "name": tool['name'], + "schemaId": tool["featureSchemaId"], + "name": tool["name"], "dataRow": { "id": data_row_id_factory(idx), }, - 'tool': tool + "tool": tool, } return result @pytest.fixture def polygon_inference(prediction_id_mapping): - polygon = prediction_id_mapping['polygon'].copy() - polygon.update({ - "polygon": [{ - "x": 147.692, - "y": 118.154 - }, { - "x": 142.769, - "y": 104.923 - }, { - "x": 57.846, - "y": 118.769 - }, { - "x": 28.308, - "y": 169.846 - }] - }) - del polygon['tool'] + polygon = prediction_id_mapping["polygon"].copy() + polygon.update( + { + "polygon": [ + {"x": 147.692, "y": 118.154}, + {"x": 142.769, "y": 104.923}, + {"x": 57.846, "y": 118.769}, + {"x": 28.308, "y": 169.846}, + ] + } + ) + del polygon["tool"] return polygon def find_tool_by_name(tool_instances, name): for tool in tool_instances: - if tool['name'] == name: + if tool["name"] == name: return tool return None @pytest.fixture def rectangle_inference(prediction_id_mapping): - tool_instance = find_tool_by_name(prediction_id_mapping['rectangle'], - 'bbox') + tool_instance = find_tool_by_name(prediction_id_mapping["rectangle"], "bbox") rectangle = tool_instance.copy() - rectangle.update({ - "bbox": { - "top": 48, - "left": 58, - "height": 65, - "width": 12 - }, - 'classifications': [{ - "schemaId": - rectangle['tool']['classifications'][0]['featureSchemaId'], - "name": - rectangle['tool']['classifications'][0]['name'], - "answer": { - "schemaId": - rectangle['tool']['classifications'][0]['options'][0] - ['featureSchemaId'], - "name": - rectangle['tool']['classifications'][0]['options'][0] - ['value'], - "customMetrics": [{ - "name": "customMetric1", - "value": 0.4 - }], - } - }] - }) - del rectangle['tool'] + rectangle.update( + { + "bbox": {"top": 48, "left": 58, "height": 65, "width": 12}, + "classifications": [ + { + "schemaId": rectangle["tool"]["classifications"][0][ + "featureSchemaId" + ], + "name": rectangle["tool"]["classifications"][0]["name"], + "answer": { + "schemaId": rectangle["tool"]["classifications"][0]["options"][ + 0 + ]["featureSchemaId"], + "name": rectangle["tool"]["classifications"][0]["options"][0][ + "value" + ], + "customMetrics": [{"name": "customMetric1", "value": 0.4}], + }, + } + ], + } + ) + del rectangle["tool"] return rectangle @pytest.fixture def rectangle_inference_with_confidence(prediction_id_mapping): - tool_instance = find_tool_by_name(prediction_id_mapping['rectangle'], - 'bbox_tool_with_nested_text') + tool_instance = find_tool_by_name( + prediction_id_mapping["rectangle"], "bbox_tool_with_nested_text" + ) rectangle = tool_instance.copy() - rectangle.update({ - "bbox": { - "top": 48, - "left": 58, - "height": 65, - "width": 12 - }, - 'classifications': [{ - "schemaId": - rectangle['tool']['classifications'][0]['featureSchemaId'], - "name": - rectangle['tool']['classifications'][0]['name'], - "answer": { - "schemaId": - rectangle['tool']['classifications'][0]['options'][0] - ['featureSchemaId'], - "name": - rectangle['tool']['classifications'][0]['options'][0] - ['value'], - "classifications": [{ - "schemaId": - rectangle['tool']['classifications'][0]['options'][0] - ['options'][1]['featureSchemaId'], - "name": - rectangle['tool']['classifications'][0]['options'][0] - ['options'][1]['name'], - "answer": - 'nested answer' - }], - } - }] - }) + rectangle.update( + { + "bbox": {"top": 48, "left": 58, "height": 65, "width": 12}, + "classifications": [ + { + "schemaId": rectangle["tool"]["classifications"][0][ + "featureSchemaId" + ], + "name": rectangle["tool"]["classifications"][0]["name"], + "answer": { + "schemaId": rectangle["tool"]["classifications"][0]["options"][ + 0 + ]["featureSchemaId"], + "name": rectangle["tool"]["classifications"][0]["options"][0][ + "value" + ], + "classifications": [ + { + "schemaId": rectangle["tool"]["classifications"][0][ + "options" + ][0]["options"][1]["featureSchemaId"], + "name": rectangle["tool"]["classifications"][0][ + "options" + ][0]["options"][1]["name"], + "answer": "nested answer", + } + ], + }, + } + ], + } + ) rectangle.update({"confidence": 0.9}) rectangle["classifications"][0]["answer"]["confidence"] = 0.8 - rectangle["classifications"][0]["answer"]["classifications"][0][ - "confidence"] = 0.7 + rectangle["classifications"][0]["answer"]["classifications"][0]["confidence"] = 0.7 - del rectangle['tool'] + del rectangle["tool"] return rectangle @@ -865,233 +831,240 @@ def rectangle_inference_document(rectangle_inference): @pytest.fixture def line_inference(prediction_id_mapping): - line = prediction_id_mapping['line'].copy() - line.update( - {"line": [{ - "x": 147.692, - "y": 118.154 - }, { - "x": 150.692, - "y": 160.154 - }]}) - del line['tool'] + line = prediction_id_mapping["line"].copy() + line.update({"line": [{"x": 147.692, "y": 118.154}, {"x": 150.692, "y": 160.154}]}) + del line["tool"] return line @pytest.fixture def line_inference_v2(prediction_id_mapping): - line = prediction_id_mapping['line'].copy() + line = prediction_id_mapping["line"].copy() line_data = { - "groupKey": - "axial", - "segments": [{ - "keyframes": [{ - "frame": - 1, - "line": [{ - "x": 147.692, - "y": 118.154 - }, { - "x": 150.692, - "y": 160.154 - }] - }] - },] + "groupKey": "axial", + "segments": [ + { + "keyframes": [ + { + "frame": 1, + "line": [ + {"x": 147.692, "y": 118.154}, + {"x": 150.692, "y": 160.154}, + ], + } + ] + }, + ], } line.update(line_data) - del line['tool'] + del line["tool"] return line @pytest.fixture def point_inference(prediction_id_mapping): - point = prediction_id_mapping['point'].copy() + point = prediction_id_mapping["point"].copy() point.update({"point": {"x": 147.692, "y": 118.154}}) - del point['tool'] + del point["tool"] return point @pytest.fixture def entity_inference(prediction_id_mapping): - entity = prediction_id_mapping['named-entity'].copy() + entity = prediction_id_mapping["named-entity"].copy() entity.update({"location": {"start": 67, "end": 128}}) - del entity['tool'] + del entity["tool"] return entity @pytest.fixture def entity_inference_index(prediction_id_mapping): - entity = prediction_id_mapping['named-entity'].copy() - entity.update({ - "location": { - "start": 0, - "end": 8 - }, - "messageId": "0", - }) + entity = prediction_id_mapping["named-entity"].copy() + entity.update( + { + "location": {"start": 0, "end": 8}, + "messageId": "0", + } + ) - del entity['tool'] + del entity["tool"] return entity @pytest.fixture def entity_inference_document(prediction_id_mapping): - entity = prediction_id_mapping['named-entity'].copy() + entity = prediction_id_mapping["named-entity"].copy() document_selections = { - "textSelections": [{ - "tokenIds": [ - "3f984bf3-1d61-44f5-b59a-9658a2e3440f", - "3bf00b56-ff12-4e52-8cc1-08dbddb3c3b8", - "6e1c3420-d4b7-4c5a-8fd6-ead43bf73d80", - "87a43d32-af76-4a1d-b262-5c5f4d5ace3a", - "e8606e8a-dfd9-4c49-a635-ad5c879c75d0", - "67c7c19e-4654-425d-bf17-2adb8cf02c30", - "149c5e80-3e07-49a7-ab2d-29ddfe6a38fa", - "b0e94071-2187-461e-8e76-96c58738a52c" - ], - "groupId": "2f4336f4-a07e-4e0a-a9e1-5629b03b719b", - "page": 1, - }] + "textSelections": [ + { + "tokenIds": [ + "3f984bf3-1d61-44f5-b59a-9658a2e3440f", + "3bf00b56-ff12-4e52-8cc1-08dbddb3c3b8", + "6e1c3420-d4b7-4c5a-8fd6-ead43bf73d80", + "87a43d32-af76-4a1d-b262-5c5f4d5ace3a", + "e8606e8a-dfd9-4c49-a635-ad5c879c75d0", + "67c7c19e-4654-425d-bf17-2adb8cf02c30", + "149c5e80-3e07-49a7-ab2d-29ddfe6a38fa", + "b0e94071-2187-461e-8e76-96c58738a52c", + ], + "groupId": "2f4336f4-a07e-4e0a-a9e1-5629b03b719b", + "page": 1, + } + ] } entity.update(document_selections) - del entity['tool'] + del entity["tool"] return entity @pytest.fixture def segmentation_inference(prediction_id_mapping): - segmentation = prediction_id_mapping['superpixel'].copy() - segmentation.update({ - 'mask': { - "instanceURI": - "https://storage.googleapis.com/labelbox-datasets/image_sample_data/raster_seg.png", - "colorRGB": (255, 255, 255) + segmentation = prediction_id_mapping["superpixel"].copy() + segmentation.update( + { + "mask": { + "instanceURI": "https://storage.googleapis.com/labelbox-datasets/image_sample_data/raster_seg.png", + "colorRGB": (255, 255, 255), + } } - }) - del segmentation['tool'] + ) + del segmentation["tool"] return segmentation @pytest.fixture def segmentation_inference_rle(prediction_id_mapping): - segmentation = prediction_id_mapping['superpixel'].copy() - segmentation.update({ - 'uuid': str(uuid.uuid4()), - 'mask': { - 'size': [10, 10], - 'counts': [1, 0, 10, 100] + segmentation = prediction_id_mapping["superpixel"].copy() + segmentation.update( + { + "uuid": str(uuid.uuid4()), + "mask": {"size": [10, 10], "counts": [1, 0, 10, 100]}, } - }) - del segmentation['tool'] + ) + del segmentation["tool"] return segmentation @pytest.fixture def segmentation_inference_png(prediction_id_mapping): - segmentation = prediction_id_mapping['superpixel'].copy() - segmentation.update({ - 'uuid': str(uuid.uuid4()), - 'mask': { - 'png': "somedata", + segmentation = prediction_id_mapping["superpixel"].copy() + segmentation.update( + { + "uuid": str(uuid.uuid4()), + "mask": { + "png": "somedata", + }, } - }) - del segmentation['tool'] + ) + del segmentation["tool"] return segmentation @pytest.fixture def checklist_inference(prediction_id_mapping): - checklist = prediction_id_mapping['checklist'].copy() - checklist.update({ - 'answers': [{ - 'schemaId': checklist['tool']['options'][0]['featureSchemaId'] - }] - }) - del checklist['tool'] + checklist = prediction_id_mapping["checklist"].copy() + checklist.update( + {"answers": [{"schemaId": checklist["tool"]["options"][0]["featureSchemaId"]}]} + ) + del checklist["tool"] return checklist @pytest.fixture def checklist_inference_index(prediction_id_mapping): - checklist = prediction_id_mapping['checklist_index'].copy() - checklist.update({ - 'answers': [{ - 'schemaId': checklist['tool']['options'][0]['featureSchemaId'] - }], - "messageId": "0", - }) - del checklist['tool'] + checklist = prediction_id_mapping["checklist_index"].copy() + checklist.update( + { + "answers": [ + {"schemaId": checklist["tool"]["options"][0]["featureSchemaId"]} + ], + "messageId": "0", + } + ) + del checklist["tool"] return checklist @pytest.fixture def text_inference(prediction_id_mapping): - text = prediction_id_mapping['text'].copy() - text.update({'answer': "free form text..."}) - del text['tool'] + text = prediction_id_mapping["text"].copy() + text.update({"answer": "free form text..."}) + del text["tool"] return text @pytest.fixture def text_inference_with_confidence(text_inference): text = text_inference.copy() - text.update({'confidence': 0.9}) + text.update({"confidence": 0.9}) return text @pytest.fixture def text_inference_index(prediction_id_mapping): - text = prediction_id_mapping['text_index'].copy() - text.update({'answer': "free form text...", "messageId": "0"}) - del text['tool'] + text = prediction_id_mapping["text_index"].copy() + text.update({"answer": "free form text...", "messageId": "0"}) + del text["tool"] return text @pytest.fixture def video_checklist_inference(prediction_id_mapping): - checklist = prediction_id_mapping['checklist'].copy() - checklist.update({ - 'answers': [{ - 'schemaId': checklist['tool']['options'][0]['featureSchemaId'] - }] - }) + checklist = prediction_id_mapping["checklist"].copy() + checklist.update( + {"answers": [{"schemaId": checklist["tool"]["options"][0]["featureSchemaId"]}]} + ) checklist.update( - {"frames": [{ - "start": 7, - "end": 13, - }, { - "start": 18, - "end": 19, - }]}) - del checklist['tool'] + { + "frames": [ + { + "start": 7, + "end": 13, + }, + { + "start": 18, + "end": 19, + }, + ] + } + ) + del checklist["tool"] return checklist @pytest.fixture -def model_run_predictions(polygon_inference, rectangle_inference, - line_inference): +def model_run_predictions(polygon_inference, rectangle_inference, line_inference): # Not supporting mask since there isn't a signed url representing a seg mask to upload return [polygon_inference, rectangle_inference, line_inference] @pytest.fixture -def object_predictions(polygon_inference, rectangle_inference, line_inference, - entity_inference, segmentation_inference): +def object_predictions( + polygon_inference, + rectangle_inference, + line_inference, + entity_inference, + segmentation_inference, +): return [ - polygon_inference, rectangle_inference, line_inference, - entity_inference, segmentation_inference + polygon_inference, + rectangle_inference, + line_inference, + entity_inference, + segmentation_inference, ] @pytest.fixture -def object_predictions_for_annotation_import(polygon_inference, - rectangle_inference, - line_inference, - segmentation_inference): +def object_predictions_for_annotation_import( + polygon_inference, rectangle_inference, line_inference, segmentation_inference +): return [ - polygon_inference, rectangle_inference, line_inference, - segmentation_inference + polygon_inference, + rectangle_inference, + line_inference, + segmentation_inference, ] @@ -1106,8 +1079,9 @@ def predictions(object_predictions, classification_predictions): @pytest.fixture -def predictions_with_confidence(text_inference_with_confidence, - rectangle_inference_with_confidence): +def predictions_with_confidence( + text_inference_with_confidence, rectangle_inference_with_confidence +): return [text_inference_with_confidence, rectangle_inference_with_confidence] @@ -1150,20 +1124,30 @@ def model_run_with_training_metadata(rand_gen, model): @pytest.fixture -def model_run_with_data_rows(client, configured_project, model_run_predictions, - model_run, wait_for_label_processing): +def model_run_with_data_rows( + client, + configured_project, + model_run_predictions, + model_run, + wait_for_label_processing, +): configured_project.enable_model_assisted_labeling() - use_data_row_ids = [p['dataRow']['id'] for p in model_run_predictions] + use_data_row_ids = [p["dataRow"]["id"] for p in model_run_predictions] model_run.upsert_data_rows(use_data_row_ids) upload_task = LabelImport.create_from_objects( - client, configured_project.uid, f"label-import-{uuid.uuid4()}", - model_run_predictions) + client, + configured_project.uid, + f"label-import-{uuid.uuid4()}", + model_run_predictions, + ) upload_task.wait_until_done() - assert upload_task.state == AnnotationImportState.FINISHED, "Label Import did not finish" - assert len( - upload_task.errors - ) == 0, f"Label Import {upload_task.name} failed with errors {upload_task.errors}" + assert ( + upload_task.state == AnnotationImportState.FINISHED + ), "Label Import did not finish" + assert ( + len(upload_task.errors) == 0 + ), f"Label Import {upload_task.name} failed with errors {upload_task.errors}" labels = wait_for_label_processing(configured_project) label_ids = [label.uid for label in labels] model_run.upsert_labels(label_ids) @@ -1173,21 +1157,30 @@ def model_run_with_data_rows(client, configured_project, model_run_predictions, @pytest.fixture -def model_run_with_all_project_labels(client, configured_project, - model_run_predictions, model_run, - wait_for_label_processing): +def model_run_with_all_project_labels( + client, + configured_project, + model_run_predictions, + model_run, + wait_for_label_processing, +): configured_project.enable_model_assisted_labeling() - use_data_row_ids = [p['dataRow']['id'] for p in model_run_predictions] + use_data_row_ids = [p["dataRow"]["id"] for p in model_run_predictions] model_run.upsert_data_rows(use_data_row_ids) upload_task = LabelImport.create_from_objects( - client, configured_project.uid, f"label-import-{uuid.uuid4()}", - model_run_predictions) + client, + configured_project.uid, + f"label-import-{uuid.uuid4()}", + model_run_predictions, + ) upload_task.wait_until_done() - assert upload_task.state == AnnotationImportState.FINISHED, "Label Import did not finish" - assert len( - upload_task.errors - ) == 0, f"Label Import {upload_task.name} failed with errors {upload_task.errors}" + assert ( + upload_task.state == AnnotationImportState.FINISHED + ), "Label Import did not finish" + assert ( + len(upload_task.errors) == 0 + ), f"Label Import {upload_task.name} failed with errors {upload_task.errors}" wait_for_label_processing(configured_project) model_run.upsert_labels(project_id=configured_project.uid) yield model_run @@ -1196,7 +1189,6 @@ def model_run_with_all_project_labels(client, configured_project, class AnnotationImportTestHelpers: - @classmethod def assert_file_content(cls, url: str, predictions): response = requests.get(url) @@ -1217,8 +1209,8 @@ def download_and_assert_status(status_file_url): response = requests.get(status_file_url) assert response.status_code == 200 for line in parser.loads(response.content): - status = line['status'] - assert status.upper() == 'SUCCESS' + status = line["status"] + assert status.upper() == "SUCCESS" @staticmethod def _convert_to_plain_object(obj): @@ -1230,3 +1222,450 @@ def _convert_to_plain_object(obj): @pytest.fixture def annotation_import_test_helpers() -> Type[AnnotationImportTestHelpers]: return AnnotationImportTestHelpers() + + +@pytest.fixture() +def expected_export_v2_image(): + exported_annotations = { + "objects": [ + { + "name": "polygon", + "value": "polygon", + "annotation_kind": "ImagePolygon", + "classifications": [], + "polygon": [ + {"x": 147.692, "y": 118.154}, + {"x": 142.769, "y": 104.923}, + {"x": 57.846, "y": 118.769}, + {"x": 28.308, "y": 169.846}, + {"x": 147.692, "y": 118.154}, + ], + }, + { + "name": "bbox", + "value": "bbox", + "annotation_kind": "ImageBoundingBox", + "classifications": [ + { + "name": "nested", + "value": "nested", + "radio_answer": { + "name": "radio_option_1", + "value": "radio_value_1", + "classifications": [], + }, + } + ], + "bounding_box": { + "top": 48.0, + "left": 58.0, + "height": 65.0, + "width": 12.0, + }, + }, + { + "name": "polyline", + "value": "polyline", + "annotation_kind": "ImagePolyline", + "classifications": [], + "line": [{"x": 147.692, "y": 118.154}, {"x": 150.692, "y": 160.154}], + }, + ], + "classifications": [ + { + "name": "checklist", + "value": "checklist", + "checklist_answers": [ + {"name": "option1", "value": "option1", "classifications": []} + ], + }, + { + "name": "text", + "value": "text", + "text_answer": {"content": "free form text..."}, + }, + ], + "relationships": [], + } + + return exported_annotations + + +@pytest.fixture() +def expected_export_v2_audio(): + expected_annotations = { + "objects": [], + "classifications": [ + { + "name": "checklist", + "value": "checklist", + "checklist_answers": [ + {"name": "option1", "value": "option1", "classifications": []} + ], + }, + { + "name": "text", + "value": "text", + "text_answer": {"content": "free form text..."}, + }, + ], + "relationships": [], + } + return expected_annotations + + +@pytest.fixture() +def expected_export_v2_html(): + expected_annotations = { + "objects": [], + "classifications": [ + { + "name": "text", + "value": "text", + "text_answer": {"content": "free form text..."}, + }, + { + "name": "checklist", + "value": "checklist", + "checklist_answers": [ + {"name": "option1", "value": "option1", "classifications": []} + ], + }, + ], + "relationships": [], + } + return expected_annotations + + +@pytest.fixture() +def expected_export_v2_text(): + expected_annotations = { + "objects": [ + { + "name": "named-entity", + "value": "named_entity", + "annotation_kind": "TextEntity", + "classifications": [], + "location": {"start": 67, "end": 128}, + } + ], + "classifications": [ + { + "name": "checklist", + "value": "checklist", + "checklist_answers": [ + {"name": "option1", "value": "option1", "classifications": []} + ], + }, + { + "name": "text", + "value": "text", + "text_answer": {"content": "free form text..."}, + }, + ], + "relationships": [], + } + return expected_annotations + + +@pytest.fixture() +def expected_export_v2_video(): + expected_annotations = { + "frames": {}, + "segments": {"": [[7, 13], [18, 19]]}, + "key_frame_feature_map": {}, + "classifications": [ + { + "name": "checklist", + "value": "checklist", + "checklist_answers": [ + {"name": "option1", "value": "option1", "classifications": []} + ], + } + ], + } + return expected_annotations + + +@pytest.fixture() +def expected_export_v2_conversation(): + expected_annotations = { + "objects": [ + { + "name": "named-entity", + "value": "named_entity", + "annotation_kind": "ConversationalTextEntity", + "classifications": [], + "conversational_location": { + "message_id": "0", + "location": {"start": 0, "end": 8}, + }, + } + ], + "classifications": [ + { + "name": "checklist_index", + "value": "checklist_index", + "message_id": "0", + "conversational_checklist_answers": [ + { + "name": "option1_index", + "value": "option1_index", + "classifications": [], + } + ], + }, + { + "name": "text_index", + "value": "text_index", + "message_id": "0", + "conversational_text_answer": {"content": "free form text..."}, + }, + ], + "relationships": [], + } + return expected_annotations + + +@pytest.fixture() +def expected_export_v2_dicom(): + expected_annotations = { + "groups": { + "Axial": { + "name": "Axial", + "classifications": [], + "frames": { + "1": { + "objects": { + "": { + "name": "polyline", + "value": "polyline", + "annotation_kind": "DICOMPolyline", + "classifications": [], + "line": [ + {"x": 147.692, "y": 118.154}, + {"x": 150.692, "y": 160.154}, + ], + } + }, + "classifications": [], + } + }, + }, + "Sagittal": {"name": "Sagittal", "classifications": [], "frames": {}}, + "Coronal": {"name": "Coronal", "classifications": [], "frames": {}}, + }, + "segments": {"Axial": {"": [[1, 1]]}, "Sagittal": {}, "Coronal": {}}, + "classifications": [], + "key_frame_feature_map": { + "": {"Axial": {"1": True}, "Coronal": {}, "Sagittal": {}} + }, + } + return expected_annotations + + +@pytest.fixture() +def expected_export_v2_document(): + expected_annotations = { + "objects": [ + { + "name": "named-entity", + "value": "named_entity", + "annotation_kind": "DocumentEntityToken", + "classifications": [], + "location": { + "groups": [ + { + "id": "2f4336f4-a07e-4e0a-a9e1-5629b03b719b", + "page_number": 1, + "tokens": [ + "3f984bf3-1d61-44f5-b59a-9658a2e3440f", + "3bf00b56-ff12-4e52-8cc1-08dbddb3c3b8", + "6e1c3420-d4b7-4c5a-8fd6-ead43bf73d80", + "87a43d32-af76-4a1d-b262-5c5f4d5ace3a", + "e8606e8a-dfd9-4c49-a635-ad5c879c75d0", + "67c7c19e-4654-425d-bf17-2adb8cf02c30", + "149c5e80-3e07-49a7-ab2d-29ddfe6a38fa", + "b0e94071-2187-461e-8e76-96c58738a52c", + ], + "text": "Metal-insulator (MI) transitions have been one of the", + } + ] + }, + }, + { + "name": "bbox", + "value": "bbox", + "annotation_kind": "DocumentBoundingBox", + "classifications": [ + { + "name": "nested", + "value": "nested", + "radio_answer": { + "name": "radio_option_1", + "value": "radio_value_1", + "classifications": [], + }, + } + ], + "page_number": 1, + "bounding_box": { + "top": 48.0, + "left": 58.0, + "height": 65.0, + "width": 12.0, + }, + }, + ], + "classifications": [ + { + "name": "checklist", + "value": "checklist", + "checklist_answers": [ + {"name": "option1", "value": "option1", "classifications": []} + ], + }, + { + "name": "text", + "value": "text", + "text_answer": {"content": "free form text..."}, + }, + ], + "relationships": [], + } + return expected_annotations + + +@pytest.fixture() +def expected_export_v2_llm_prompt_creation(): + expected_annotations = { + "objects": [], + "classifications": [ + { + "name": "checklist", + "value": "checklist", + "checklist_answers": [ + {"name": "option1", "value": "option1", "classifications": []} + ], + }, + { + "name": "text", + "value": "text", + "text_answer": {"content": "free form text..."}, + }, + ], + "relationships": [], + } + return expected_annotations + + +@pytest.fixture() +def expected_export_v2_llm_prompt_response_creation(): + expected_annotations = { + "objects": [], + "classifications": [ + { + "name": "checklist", + "value": "checklist", + "checklist_answers": [ + {"name": "option1", "value": "option1", "classifications": []} + ], + }, + { + "name": "text", + "value": "text", + "text_answer": {"content": "free form text..."}, + }, + ], + "relationships": [], + } + return expected_annotations + + +@pytest.fixture() +def expected_export_v2_llm_response_creation(): + expected_annotations = { + "objects": [], + "classifications": [ + { + "name": "checklist", + "value": "checklist", + "checklist_answers": [ + {"name": "option1", "value": "option1", "classifications": []} + ], + }, + { + "name": "text", + "value": "text", + "text_answer": {"content": "free form text..."}, + }, + ], + "relationships": [], + } + return expected_annotations + + +import pytest +from labelbox.data.annotation_types.classification.classification import ( + Checklist, + ClassificationAnnotation, + ClassificationAnswer, + Radio, +) +from labelbox.data.annotation_types.geometry.point import Point +from labelbox.data.annotation_types.geometry.rectangle import Rectangle + +from labelbox.data.annotation_types.video import VideoObjectAnnotation + + +@pytest.fixture +def bbox_video_annotation_objects(): + bbox_annotation = [ + VideoObjectAnnotation( + name="bbox", + keyframe=True, + frame=13, + segment_index=0, + value=Rectangle( + start=Point(x=146.0, y=98.0), # Top left + end=Point(x=382.0, y=341.0), # Bottom right + ), + classifications=[ + ClassificationAnnotation( + name="nested", + value=Radio( + answer=ClassificationAnswer( + name="radio_option_1", + classifications=[ + ClassificationAnnotation( + name="nested_checkbox", + value=Checklist( + answer=[ + ClassificationAnswer( + name="nested_checkbox_option_1" + ), + ClassificationAnswer( + name="nested_checkbox_option_2" + ), + ] + ), + ) + ], + ) + ), + ) + ], + ), + VideoObjectAnnotation( + name="bbox", + keyframe=True, + frame=19, + segment_index=0, + value=Rectangle( + start=Point(x=186.0, y=98.0), # Top left + end=Point(x=490.0, y=341.0), # Bottom right + ), + ), + ] + + return bbox_annotation diff --git a/libs/labelbox/tests/data/annotation_import/fixtures/__init__.py b/libs/labelbox/tests/data/annotation_import/fixtures/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/libs/labelbox/tests/data/annotation_import/fixtures/export_v2.py b/libs/labelbox/tests/data/annotation_import/fixtures/export_v2.py deleted file mode 100644 index 616351c5cb..0000000000 --- a/libs/labelbox/tests/data/annotation_import/fixtures/export_v2.py +++ /dev/null @@ -1,446 +0,0 @@ -import pytest - - -@pytest.fixture() -def expected_export_v2_image(): - exported_annotations = { - 'objects': [{ - 'name': - 'polygon', - 'value': - 'polygon', - 'annotation_kind': - 'ImagePolygon', - 'classifications': [], - 'polygon': [{ - 'x': 147.692, - 'y': 118.154 - }, { - 'x': 142.769, - 'y': 104.923 - }, { - 'x': 57.846, - 'y': 118.769 - }, { - 'x': 28.308, - 'y': 169.846 - }, { - 'x': 147.692, - 'y': 118.154 - }] - }, { - 'name': 'bbox', - 'value': 'bbox', - 'annotation_kind': 'ImageBoundingBox', - 'classifications': [{ - 'name': 'nested', - 'value': 'nested', - 'radio_answer': { - 'name': 'radio_option_1', - 'value': 'radio_value_1', - 'classifications': [] - } - }], - 'bounding_box': { - 'top': 48.0, - 'left': 58.0, - 'height': 65.0, - 'width': 12.0 - } - }, { - 'name': 'polyline', - 'value': 'polyline', - 'annotation_kind': 'ImagePolyline', - 'classifications': [], - 'line': [{ - 'x': 147.692, - 'y': 118.154 - }, { - 'x': 150.692, - 'y': 160.154 - }] - }], - 'classifications': [{ - 'name': - 'checklist', - 'value': - 'checklist', - 'checklist_answers': [{ - 'name': 'option1', - 'value': 'option1', - 'classifications': [] - }] - }, { - 'name': 'text', - 'value': 'text', - 'text_answer': { - 'content': 'free form text...' - } - }], - 'relationships': [] - } - - return exported_annotations - - -@pytest.fixture() -def expected_export_v2_audio(): - expected_annotations = { - 'objects': [], - 'classifications': [{ - 'name': - 'checklist', - 'value': - 'checklist', - 'checklist_answers': [{ - 'name': 'option1', - 'value': 'option1', - 'classifications': [] - }] - }, { - 'name': 'text', - 'value': 'text', - 'text_answer': { - 'content': 'free form text...' - } - }], - 'relationships': [] - } - return expected_annotations - - -@pytest.fixture() -def expected_export_v2_html(): - expected_annotations = { - 'objects': [], - 'classifications': [{ - 'name': 'text', - 'value': 'text', - 'text_answer': { - 'content': 'free form text...' - } - }, { - 'name': - 'checklist', - 'value': - 'checklist', - 'checklist_answers': [{ - 'name': 'option1', - 'value': 'option1', - 'classifications': [] - }] - }], - 'relationships': [] - } - return expected_annotations - - -@pytest.fixture() -def expected_export_v2_text(): - expected_annotations = { - 'objects': [{ - 'name': 'named-entity', - 'value': 'named_entity', - 'annotation_kind': 'TextEntity', - 'classifications': [], - 'location': { - 'start': 67, - 'end': 128 - } - }], - 'classifications': [{ - 'name': - 'checklist', - 'value': - 'checklist', - 'checklist_answers': [{ - 'name': 'option1', - 'value': 'option1', - 'classifications': [] - }] - }, { - 'name': 'text', - 'value': 'text', - 'text_answer': { - 'content': 'free form text...' - } - }], - 'relationships': [] - } - return expected_annotations - - -@pytest.fixture() -def expected_export_v2_video(): - expected_annotations = { - 'frames': {}, - 'segments': { - '': [[7, 13], [18, 19]] - }, - 'key_frame_feature_map': {}, - 'classifications': [{ - 'name': - 'checklist', - 'value': - 'checklist', - 'checklist_answers': [{ - 'name': 'option1', - 'value': 'option1', - 'classifications': [] - }] - }] - } - return expected_annotations - - -@pytest.fixture() -def expected_export_v2_conversation(): - expected_annotations = { - 'objects': [{ - 'name': 'named-entity', - 'value': 'named_entity', - 'annotation_kind': 'ConversationalTextEntity', - 'classifications': [], - 'conversational_location': { - 'message_id': '0', - 'location': { - 'start': 0, - 'end': 8 - } - } - }], - 'classifications': [{ - 'name': - 'checklist_index', - 'value': - 'checklist_index', - 'message_id': - '0', - 'conversational_checklist_answers': [{ - 'name': 'option1_index', - 'value': 'option1_index', - 'classifications': [] - }] - }, { - 'name': 'text_index', - 'value': 'text_index', - 'message_id': '0', - 'conversational_text_answer': { - 'content': 'free form text...' - } - }], - 'relationships': [] - } - return expected_annotations - - -@pytest.fixture() -def expected_export_v2_dicom(): - expected_annotations = { - 'groups': { - 'Axial': { - 'name': 'Axial', - 'classifications': [], - 'frames': { - '1': { - 'objects': { - '': { - 'name': - 'polyline', - 'value': - 'polyline', - 'annotation_kind': - 'DICOMPolyline', - 'classifications': [], - 'line': [{ - 'x': 147.692, - 'y': 118.154 - }, { - 'x': 150.692, - 'y': 160.154 - }] - } - }, - 'classifications': [] - } - } - }, - 'Sagittal': { - 'name': 'Sagittal', - 'classifications': [], - 'frames': {} - }, - 'Coronal': { - 'name': 'Coronal', - 'classifications': [], - 'frames': {} - } - }, - 'segments': { - 'Axial': { - '': [[1, 1]] - }, - 'Sagittal': {}, - 'Coronal': {} - }, - 'classifications': [], - 'key_frame_feature_map': { - '': { - 'Axial': { - '1': True - }, - 'Coronal': {}, - 'Sagittal': {} - } - } - } - return expected_annotations - - -@pytest.fixture() -def expected_export_v2_document(): - expected_annotations = { - 'objects': [{ - 'name': 'named-entity', - 'value': 'named_entity', - 'annotation_kind': 'DocumentEntityToken', - 'classifications': [], - 'location': { - 'groups': [{ - 'id': - '2f4336f4-a07e-4e0a-a9e1-5629b03b719b', - 'page_number': - 1, - 'tokens': [ - '3f984bf3-1d61-44f5-b59a-9658a2e3440f', - '3bf00b56-ff12-4e52-8cc1-08dbddb3c3b8', - '6e1c3420-d4b7-4c5a-8fd6-ead43bf73d80', - '87a43d32-af76-4a1d-b262-5c5f4d5ace3a', - 'e8606e8a-dfd9-4c49-a635-ad5c879c75d0', - '67c7c19e-4654-425d-bf17-2adb8cf02c30', - '149c5e80-3e07-49a7-ab2d-29ddfe6a38fa', - 'b0e94071-2187-461e-8e76-96c58738a52c' - ], - 'text': - 'Metal-insulator (MI) transitions have been one of the' - }] - } - }, { - 'name': 'bbox', - 'value': 'bbox', - 'annotation_kind': 'DocumentBoundingBox', - 'classifications': [{ - 'name': 'nested', - 'value': 'nested', - 'radio_answer': { - 'name': 'radio_option_1', - 'value': 'radio_value_1', - 'classifications': [] - } - }], - 'page_number': 1, - 'bounding_box': { - 'top': 48.0, - 'left': 58.0, - 'height': 65.0, - 'width': 12.0 - } - }], - 'classifications': [{ - 'name': - 'checklist', - 'value': - 'checklist', - 'checklist_answers': [{ - 'name': 'option1', - 'value': 'option1', - 'classifications': [] - }] - }, { - 'name': 'text', - 'value': 'text', - 'text_answer': { - 'content': 'free form text...' - } - }], - 'relationships': [] - } - return expected_annotations - - -@pytest.fixture() -def expected_export_v2_llm_prompt_creation(): - expected_annotations = { - 'objects': [], - 'classifications': [{ - 'name': - 'checklist', - 'value': - 'checklist', - 'checklist_answers': [{ - 'name': 'option1', - 'value': 'option1', - 'classifications': [] - }] - }, { - 'name': 'text', - 'value': 'text', - 'text_answer': { - 'content': 'free form text...' - } - }], - 'relationships': [] - } - return expected_annotations - - -@pytest.fixture() -def expected_export_v2_llm_prompt_response_creation(): - expected_annotations = { - 'objects': [], - 'classifications': [{ - 'name': - 'checklist', - 'value': - 'checklist', - 'checklist_answers': [{ - 'name': 'option1', - 'value': 'option1', - 'classifications': [] - }] - }, { - 'name': 'text', - 'value': 'text', - 'text_answer': { - 'content': 'free form text...' - } - }], - 'relationships': [] - } - return expected_annotations - - -@pytest.fixture() -def expected_export_v2_llm_response_creation(): - expected_annotations = { - 'objects': [], - 'classifications': [{ - 'name': - 'checklist', - 'value': - 'checklist', - 'checklist_answers': [{ - 'name': 'option1', - 'value': 'option1', - 'classifications': [] - }] - }, { - 'name': 'text', - 'value': 'text', - 'text_answer': { - 'content': 'free form text...' - } - }], - 'relationships': [] - } - return expected_annotations diff --git a/libs/labelbox/tests/data/annotation_import/fixtures/video_annotations.py b/libs/labelbox/tests/data/annotation_import/fixtures/video_annotations.py deleted file mode 100644 index 53c48c184a..0000000000 --- a/libs/labelbox/tests/data/annotation_import/fixtures/video_annotations.py +++ /dev/null @@ -1,49 +0,0 @@ -import pytest -from labelbox.data.annotation_types.classification.classification import Checklist, ClassificationAnnotation, ClassificationAnswer, Radio -from labelbox.data.annotation_types.geometry.point import Point -from labelbox.data.annotation_types.geometry.rectangle import Rectangle - -from labelbox.data.annotation_types.video import VideoObjectAnnotation - - -@pytest.fixture -def bbox_video_annotation_objects(): - bbox_annotation = [ - VideoObjectAnnotation( - name="bbox", - keyframe=True, - frame=13, - segment_index=0, - value=Rectangle( - start=Point(x=146.0, y=98.0), # Top left - end=Point(x=382.0, y=341.0), # Bottom right - ), - classifications=[ - ClassificationAnnotation( - name='nested', - value=Radio(answer=ClassificationAnswer( - name='radio_option_1', - classifications=[ - ClassificationAnnotation( - name='nested_checkbox', - value=Checklist(answer=[ - ClassificationAnswer( - name='nested_checkbox_option_1'), - ClassificationAnswer( - name='nested_checkbox_option_2') - ])) - ])), - ) - ]), - VideoObjectAnnotation( - name="bbox", - keyframe=True, - frame=19, - segment_index=0, - value=Rectangle( - start=Point(x=186.0, y=98.0), # Top left - end=Point(x=490.0, y=341.0), # Bottom right - )) - ] - - return bbox_annotation diff --git a/libs/labelbox/tests/data/annotation_import/test_data_types.py b/libs/labelbox/tests/data/annotation_import/test_data_types.py index eaf71e55d5..b30265ce96 100644 --- a/libs/labelbox/tests/data/annotation_import/test_data_types.py +++ b/libs/labelbox/tests/data/annotation_import/test_data_types.py @@ -8,113 +8,163 @@ from labelbox.schema.data_row import DataRow from labelbox.schema.media_type import MediaType import labelbox.types as lb_types -from labelbox.data.annotation_types.data import AudioData, ConversationData, DicomData, DocumentData, HTMLData, ImageData, TextData, LlmPromptCreationData, LlmPromptResponseCreationData, LlmResponseCreationData +from labelbox.data.annotation_types.data import ( + AudioData, + ConversationData, + DicomData, + DocumentData, + HTMLData, + ImageData, + TextData, + LlmPromptCreationData, + LlmPromptResponseCreationData, + LlmResponseCreationData, +) from labelbox.data.serialization import NDJsonConverter from labelbox.schema.annotation_import import AnnotationImportState -from utils import remove_keys_recursive, rename_cuid_key_recursive DATA_ROW_PROCESSING_WAIT_TIMEOUT_SECONDS = 40 DATA_ROW_PROCESSING_WAIT_SLEEP_INTERNAL_SECONDS = 7 radio_annotation = lb_types.ClassificationAnnotation( name="radio", - value=lb_types.Radio(answer=lb_types.ClassificationAnswer( - name="second_radio_answer"))) + value=lb_types.Radio( + answer=lb_types.ClassificationAnswer(name="second_radio_answer") + ), +) checklist_annotation = lb_types.ClassificationAnnotation( name="checklist", - value=lb_types.Checklist(answer=[ - lb_types.ClassificationAnswer(name="option1"), - lb_types.ClassificationAnswer(name="option2") - ])) + value=lb_types.Checklist( + answer=[ + lb_types.ClassificationAnswer(name="option1"), + lb_types.ClassificationAnswer(name="option2"), + ] + ), +) text_annotation = lb_types.ClassificationAnnotation( - name="text", value=lb_types.Text(answer="sample text")) - -video_mask_annotation = lb_types.VideoMaskAnnotation(frames=[ - lb_types.MaskFrame( - index=10, - instance_uri= - "https://storage.googleapis.com/labelbox-datasets/video-sample-data/mask_example.png" - ) -], - instances=[ - lb_types.MaskInstance( - color_rgb=(255, - 255, - 255), - name= - "segmentation_mask" - ) - ]) - -test_params = [[ - 'html', lb_types.HTMLData, - [radio_annotation, checklist_annotation, text_annotation] -], - [ - 'audio', lb_types.AudioData, - [radio_annotation, checklist_annotation, text_annotation] - ], ['video', lb_types.VideoData, [video_mask_annotation]]] + name="text", value=lb_types.Text(answer="sample text") +) + +video_mask_annotation = lb_types.VideoMaskAnnotation( + frames=[ + lb_types.MaskFrame( + index=10, + instance_uri="https://storage.googleapis.com/labelbox-datasets/video-sample-data/mask_example.png", + ) + ], + instances=[ + lb_types.MaskInstance(color_rgb=(255, 255, 255), name="segmentation_mask") + ], +) + +test_params = [ + [ + "html", + lb_types.HTMLData, + [radio_annotation, checklist_annotation, text_annotation], + ], + [ + "audio", + lb_types.AudioData, + [radio_annotation, checklist_annotation, text_annotation], + ], + ["video", lb_types.VideoData, [video_mask_annotation]], +] + + +def remove_keys_recursive(d, keys): + for k in keys: + if k in d: + del d[k] + for k, v in d.items(): + if isinstance(v, dict): + remove_keys_recursive(v, keys) + elif isinstance(v, list): + for i in v: + if isinstance(i, dict): + remove_keys_recursive(i, keys) + + +# NOTE this uses quite a primitive check for cuids but I do not think it is worth coming up with a better one +# Also this function is NOT written with performance in mind, good for small to mid size dicts like we have in our test +def rename_cuid_key_recursive(d): + new_key = "" + for k in list(d.keys()): + if len(k) == 25 and not k.isalpha(): # primitive check for cuid + d[new_key] = d.pop(k) + for k, v in d.items(): + if isinstance(v, dict): + rename_cuid_key_recursive(v) + elif isinstance(v, list): + for i in v: + if isinstance(i, dict): + rename_cuid_key_recursive(i) def get_annotation_comparison_dicts_from_labels(labels): labels_ndjson = list(NDJsonConverter.serialize(labels)) for annotation in labels_ndjson: - annotation.pop('uuid', None) - annotation.pop('dataRow') - - if 'masks' in annotation: - for frame in annotation['masks']['frames']: - frame.pop('instanceURI') - frame.pop('imBytes') - for instance in annotation['masks']['instances']: - instance.pop('colorRGB') + annotation.pop("uuid", None) + annotation.pop("dataRow") + + if "masks" in annotation: + for frame in annotation["masks"]["frames"]: + frame.pop("instanceURI") + frame.pop("imBytes") + for instance in annotation["masks"]["instances"]: + instance.pop("colorRGB") return labels_ndjson -def get_annotation_comparison_dicts_from_export(export_result, data_row_id, - project_id): +def get_annotation_comparison_dicts_from_export(export_result, data_row_id, project_id): exported_data_row = [ - dr for dr in export_result if dr['data_row']['id'] == data_row_id + dr for dr in export_result if dr["data_row"]["id"] == data_row_id ][0] - exported_label = exported_data_row['projects'][project_id]['labels'][0] - exported_annotations = exported_label['annotations'] + exported_label = exported_data_row["projects"][project_id]["labels"][0] + exported_annotations = exported_label["annotations"] converted_annotations = [] - if exported_label['label_kind'] == 'Video': + if exported_label["label_kind"] == "Video": frames = [] instances = [] - for frame_id, frame in exported_annotations['frames'].items(): - frames.append({'index': int(frame_id)}) - for object in frame['objects'].values(): - instances.append({'name': object['name']}) + for frame_id, frame in exported_annotations["frames"].items(): + frames.append({"index": int(frame_id)}) + for object in frame["objects"].values(): + instances.append({"name": object["name"]}) converted_annotations.append( - {'masks': { - 'frames': frames, - 'instances': instances, - }}) + { + "masks": { + "frames": frames, + "instances": instances, + } + } + ) else: - exported_annotations = list( - itertools.chain(*exported_annotations.values())) + exported_annotations = list(itertools.chain(*exported_annotations.values())) for annotation in exported_annotations: - if annotation['name'] == 'radio': - converted_annotations.append({ - 'name': annotation['name'], - 'answer': { - 'name': annotation['radio_answer']['name'] + if annotation["name"] == "radio": + converted_annotations.append( + { + "name": annotation["name"], + "answer": {"name": annotation["radio_answer"]["name"]}, + } + ) + elif annotation["name"] == "checklist": + converted_annotations.append( + { + "name": annotation["name"], + "answer": [ + {"name": answer["name"]} + for answer in annotation["checklist_answers"] + ], } - }) - elif annotation['name'] == 'checklist': - converted_annotations.append({ - 'name': - annotation['name'], - 'answer': [{ - 'name': answer['name'] - } for answer in annotation['checklist_answers']] - }) - elif annotation['name'] == 'text': - converted_annotations.append({ - 'name': annotation['name'], - 'answer': annotation['text_answer']['content'] - }) + ) + elif annotation["name"] == "text": + converted_annotations.append( + { + "name": annotation["name"], + "answer": annotation["text_answer"]["content"], + } + ) return converted_annotations @@ -124,7 +174,7 @@ def create_data_row_for_project(project, dataset, data_row_ndjson, batch_name): project.create_batch( batch_name, [data_row.uid], # sample of data row objects - 5 # priority between 1(Highest) - 5(lowest) + 5, # priority between 1(Highest) - 5(lowest) ) project.data_row_ids.append(data_row.uid) @@ -132,11 +182,21 @@ def create_data_row_for_project(project, dataset, data_row_ndjson, batch_name): # TODO: Add VideoData. Currently label import job finishes without errors but project.export_labels() returns empty list. -@pytest.mark.parametrize('data_type_class', [ - AudioData, ConversationData, DicomData, DocumentData, HTMLData, ImageData, - TextData, LlmPromptCreationData, LlmPromptResponseCreationData, - LlmResponseCreationData -]) +@pytest.mark.parametrize( + "data_type_class", + [ + AudioData, + ConversationData, + DicomData, + DocumentData, + HTMLData, + ImageData, + TextData, + LlmPromptCreationData, + LlmPromptResponseCreationData, + LlmResponseCreationData, + ], +) def test_import_data_types( client, configured_project, @@ -146,7 +206,6 @@ def test_import_data_types( annotations_by_data_type, data_type_class, ): - project = configured_project project_id = project.uid dataset = initial_dataset @@ -155,29 +214,29 @@ def test_import_data_types( data_type_string = data_type_class.__name__[:-4].lower() data_row_ndjson = data_row_json_by_data_type[data_type_string] - data_row = create_data_row_for_project(project, dataset, data_row_ndjson, - rand_gen(str)) + data_row = create_data_row_for_project( + project, dataset, data_row_ndjson, rand_gen(str) + ) annotations_ndjson = annotations_by_data_type[data_type_string] annotations_list = [ - label.annotations - for label in NDJsonConverter.deserialize(annotations_ndjson) + label.annotations for label in NDJsonConverter.deserialize(annotations_ndjson) ] labels = [ - lb_types.Label(data=data_type_class(uid=data_row.uid), - annotations=annotations) + lb_types.Label(data=data_type_class(uid=data_row.uid), annotations=annotations) for annotations in annotations_list ] label_import = lb.LabelImport.create_from_objects( - client, project_id, f'test-import-{data_type_string}', labels) + client, project_id, f"test-import-{data_type_string}", labels + ) label_import.wait_until_done() assert label_import.state == AnnotationImportState.FINISHED assert len(label_import.errors) == 0 exported_labels = project.export_labels(download=True) - objects = exported_labels[0]['Label']['objects'] - classifications = exported_labels[0]['Label']['classifications'] + objects = exported_labels[0]["Label"]["objects"] + classifications = exported_labels[0]["Label"]["classifications"] assert len(objects) + len(classifications) == len(labels) data_row.delete() @@ -190,46 +249,48 @@ def test_import_data_types_by_global_key( data_row_json_by_data_type, annotations_by_data_type, ): - project = configured_project project_id = project.uid dataset = initial_dataset data_type_class = ImageData set_project_media_type_from_data_type(project, data_type_class) - data_row_ndjson = data_row_json_by_data_type['image'] - data_row_ndjson['global_key'] = str(uuid.uuid4()) - data_row = create_data_row_for_project(project, dataset, data_row_ndjson, - rand_gen(str)) + data_row_ndjson = data_row_json_by_data_type["image"] + data_row_ndjson["global_key"] = str(uuid.uuid4()) + data_row = create_data_row_for_project( + project, dataset, data_row_ndjson, rand_gen(str) + ) - annotations_ndjson = annotations_by_data_type['image'] + annotations_ndjson = annotations_by_data_type["image"] annotations_list = [ - label.annotations - for label in NDJsonConverter.deserialize(annotations_ndjson) + label.annotations for label in NDJsonConverter.deserialize(annotations_ndjson) ] labels = [ - lb_types.Label(data=data_type_class(global_key=data_row.global_key), - annotations=annotations) + lb_types.Label( + data=data_type_class(global_key=data_row.global_key), + annotations=annotations, + ) for annotations in annotations_list ] - label_import = lb.LabelImport.create_from_objects(client, project_id, - f'test-import-image', - labels) + label_import = lb.LabelImport.create_from_objects( + client, project_id, f"test-import-image", labels + ) label_import.wait_until_done() assert label_import.state == AnnotationImportState.FINISHED assert len(label_import.errors) == 0 exported_labels = project.export_labels(download=True) - objects = exported_labels[0]['Label']['objects'] - classifications = exported_labels[0]['Label']['classifications'] + objects = exported_labels[0]["Label"]["objects"] + classifications = exported_labels[0]["Label"]["classifications"] assert len(objects) + len(classifications) == len(labels) data_row.delete() def validate_iso_format(date_string: str): parsed_t = datetime.datetime.fromisoformat( - date_string) #this will blow up if the string is not in iso format + date_string + ) # this will blow up if the string is not in iso format assert parsed_t.hour is not None assert parsed_t.minute is not None assert parsed_t.second is not None @@ -242,28 +303,44 @@ def to_pascal_case(name: str) -> str: def set_project_media_type_from_data_type(project, data_type_class): data_type_string = data_type_class.__name__[:-4].lower() media_type = to_pascal_case(data_type_string) - if media_type == 'Conversation': - media_type = 'Conversational' - elif media_type == 'Llmpromptcreation': - media_type = 'LLMPromptCreation' - elif media_type == 'Llmpromptresponsecreation': - media_type = 'LLMPromptResponseCreation' - elif media_type == 'Llmresponsecreation': - media_type = 'Text' + if media_type == "Conversation": + media_type = "Conversational" + elif media_type == "Llmpromptcreation": + media_type = "LLMPromptCreation" + elif media_type == "Llmpromptresponsecreation": + media_type = "LLMPromptResponseCreation" + elif media_type == "Llmresponsecreation": + media_type = "Text" project.update(media_type=MediaType[media_type]) -@pytest.mark.parametrize('data_type_class', [ - AudioData, HTMLData, ImageData, TextData, VideoData, ConversationData, - DocumentData, DicomData, LlmPromptCreationData, - LlmPromptResponseCreationData, LlmResponseCreationData -]) -def test_import_data_types_v2(client, configured_project, initial_dataset, - data_row_json_by_data_type, - annotations_by_data_type_v2, data_type_class, - exports_v2_by_data_type, export_v2_test_helpers, - rand_gen): - +@pytest.mark.parametrize( + "data_type_class", + [ + AudioData, + HTMLData, + ImageData, + TextData, + VideoData, + ConversationData, + DocumentData, + DicomData, + LlmPromptCreationData, + LlmPromptResponseCreationData, + LlmResponseCreationData, + ], +) +def test_import_data_types_v2( + client, + configured_project, + initial_dataset, + data_row_json_by_data_type, + annotations_by_data_type_v2, + data_type_class, + exports_v2_by_data_type, + export_v2_test_helpers, + rand_gen, +): project = configured_project dataset = initial_dataset project_id = project.uid @@ -272,47 +349,52 @@ def test_import_data_types_v2(client, configured_project, initial_dataset, data_type_string = data_type_class.__name__[:-4].lower() data_row_ndjson = data_row_json_by_data_type[data_type_string] - data_row = create_data_row_for_project(project, dataset, data_row_ndjson, - rand_gen(str)) + data_row = create_data_row_for_project( + project, dataset, data_row_ndjson, rand_gen(str) + ) annotations_ndjson = annotations_by_data_type_v2[data_type_string] annotations_list = [ - label.annotations - for label in NDJsonConverter.deserialize(annotations_ndjson) + label.annotations for label in NDJsonConverter.deserialize(annotations_ndjson) ] labels = [ - lb_types.Label(data=data_type_class(uid=data_row.uid), - annotations=annotations) + lb_types.Label(data=data_type_class(uid=data_row.uid), annotations=annotations) for annotations in annotations_list ] label_import = lb.LabelImport.create_from_objects( - client, project_id, f'test-import-{data_type_string}', labels) + client, project_id, f"test-import-{data_type_string}", labels + ) label_import.wait_until_done() assert label_import.state == AnnotationImportState.FINISHED assert len(label_import.errors) == 0 - #TODO need to migrate project to the new BATCH mode and change this code + # TODO need to migrate project to the new BATCH mode and change this code # to be similar to tests/integration/test_task_queue.py result = export_v2_test_helpers.run_project_export_v2_task(project) exported_data = result[0] # timestamp fields are in iso format - validate_iso_format(exported_data['data_row']['details']['created_at']) - validate_iso_format(exported_data['data_row']['details']['updated_at']) - validate_iso_format(exported_data['projects'][project_id]['labels'][0] - ['label_details']['created_at']) - validate_iso_format(exported_data['projects'][project_id]['labels'][0] - ['label_details']['updated_at']) - - assert (exported_data['data_row']['id'] == data_row.uid) - exported_project = exported_data['projects'][project_id] - exported_project_labels = exported_project['labels'][0] - exported_annotations = exported_project_labels['annotations'] - - remove_keys_recursive(exported_annotations, - ['feature_id', 'feature_schema_id']) + validate_iso_format(exported_data["data_row"]["details"]["created_at"]) + validate_iso_format(exported_data["data_row"]["details"]["updated_at"]) + validate_iso_format( + exported_data["projects"][project_id]["labels"][0]["label_details"][ + "created_at" + ] + ) + validate_iso_format( + exported_data["projects"][project_id]["labels"][0]["label_details"][ + "updated_at" + ] + ) + + assert exported_data["data_row"]["id"] == data_row.uid + exported_project = exported_data["projects"][project_id] + exported_project_labels = exported_project["labels"][0] + exported_annotations = exported_project_labels["annotations"] + + remove_keys_recursive(exported_annotations, ["feature_id", "feature_schema_id"]) rename_cuid_key_recursive(exported_annotations) assert exported_annotations == exports_v2_by_data_type[data_type_string] @@ -320,27 +402,33 @@ def test_import_data_types_v2(client, configured_project, initial_dataset, data_row.delete() -@pytest.mark.parametrize('data_type, data_class, annotations', test_params) -def test_import_label_annotations(client, configured_project_with_one_data_row, - initial_dataset, data_row_json_by_data_type, - data_type, data_class, annotations, rand_gen): - +@pytest.mark.parametrize("data_type, data_class, annotations", test_params) +def test_import_label_annotations( + client, + configured_project_with_one_data_row, + initial_dataset, + data_row_json_by_data_type, + data_type, + data_class, + annotations, + rand_gen, +): project = configured_project_with_one_data_row dataset = initial_dataset set_project_media_type_from_data_type(project, data_class) data_row_json = data_row_json_by_data_type[data_type] - data_row = create_data_row_for_project(project, dataset, data_row_json, - rand_gen(str)) + data_row = create_data_row_for_project( + project, dataset, data_row_json, rand_gen(str) + ) labels = [ - lb_types.Label(data=data_class(uid=data_row.uid), - annotations=annotations) + lb_types.Label(data=data_class(uid=data_row.uid), annotations=annotations) ] - label_import = lb.LabelImport.create_from_objects(client, project.uid, - f'test-import-html', - labels) + label_import = lb.LabelImport.create_from_objects( + client, project.uid, f"test-import-html", labels + ) label_import.wait_until_done() assert label_import.state == lb.AnnotationImportState.FINISHED @@ -350,20 +438,20 @@ def test_import_label_annotations(client, configured_project_with_one_data_row, "metadata_fields": False, "data_row_details": False, "project_details": False, - "performance_details": False + "performance_details": False, } export_task = project.export_v2(params=export_params) export_task.wait_till_done() assert export_task.errors is None expected_annotations = get_annotation_comparison_dicts_from_labels(labels) actual_annotations = get_annotation_comparison_dicts_from_export( - export_task.result, data_row.uid, - configured_project_with_one_data_row.uid) + export_task.result, data_row.uid, configured_project_with_one_data_row.uid + ) assert actual_annotations == expected_annotations data_row.delete() -@pytest.mark.parametrize('data_type, data_class, annotations', test_params) +@pytest.mark.parametrize("data_type, data_class, annotations", test_params) @pytest.fixture def one_datarow(client, rand_gen, data_row_json_by_data_type, data_type): dataset = client.create_dataset(name=rand_gen(str)) @@ -378,7 +466,7 @@ def one_datarow(client, rand_gen, data_row_json_by_data_type, data_type): @pytest.fixture def one_datarow_global_key(client, rand_gen, data_row_json_by_data_type): dataset = client.create_dataset(name=rand_gen(str)) - data_row_json = data_row_json_by_data_type['video'] + data_row_json = data_row_json_by_data_type["video"] data_row = dataset.create_data_row(data_row_json) yield data_row @@ -386,13 +474,20 @@ def one_datarow_global_key(client, rand_gen, data_row_json_by_data_type): dataset.delete() -@pytest.mark.parametrize('data_type, data_class, annotations', test_params) -def test_import_mal_annotations(client, configured_project_with_one_data_row, - data_type, data_class, annotations, rand_gen, - one_datarow): +@pytest.mark.parametrize("data_type, data_class, annotations", test_params) +def test_import_mal_annotations( + client, + configured_project_with_one_data_row, + data_type, + data_class, + annotations, + rand_gen, + one_datarow, +): data_row = one_datarow - set_project_media_type_from_data_type(configured_project_with_one_data_row, - data_class) + set_project_media_type_from_data_type( + configured_project_with_one_data_row, data_class + ) configured_project_with_one_data_row.create_batch( rand_gen(str), @@ -400,29 +495,30 @@ def test_import_mal_annotations(client, configured_project_with_one_data_row, ) labels = [ - lb_types.Label(data=data_class(uid=data_row.uid), - annotations=annotations) + lb_types.Label(data=data_class(uid=data_row.uid), annotations=annotations) ] import_annotations = lb.MALPredictionImport.create_from_objects( client=client, project_id=configured_project_with_one_data_row.uid, name=f"import {str(uuid.uuid4())}", - predictions=labels) + predictions=labels, + ) import_annotations.wait_until_done() assert import_annotations.errors == [] # MAL Labels cannot be exported and compared to input labels -def test_import_mal_annotations_global_key(client, - configured_project_with_one_data_row, - rand_gen, one_datarow_global_key): +def test_import_mal_annotations_global_key( + client, configured_project_with_one_data_row, rand_gen, one_datarow_global_key +): data_class = lb_types.VideoData data_row = one_datarow_global_key annotations = [video_mask_annotation] - set_project_media_type_from_data_type(configured_project_with_one_data_row, - data_class) + set_project_media_type_from_data_type( + configured_project_with_one_data_row, data_class + ) configured_project_with_one_data_row.create_batch( rand_gen(str), @@ -430,15 +526,17 @@ def test_import_mal_annotations_global_key(client, ) labels = [ - lb_types.Label(data=data_class(global_key=data_row.global_key), - annotations=annotations) + lb_types.Label( + data=data_class(global_key=data_row.global_key), annotations=annotations + ) ] import_annotations = lb.MALPredictionImport.create_from_objects( client=client, project_id=configured_project_with_one_data_row.uid, name=f"import {str(uuid.uuid4())}", - predictions=labels) + predictions=labels, + ) import_annotations.wait_until_done() assert import_annotations.errors == [] diff --git a/libs/labelbox/tests/data/annotation_import/fixtures/annotations.py b/libs/labelbox/tests/data/conftest.py similarity index 100% rename from libs/labelbox/tests/data/annotation_import/fixtures/annotations.py rename to libs/labelbox/tests/data/conftest.py index 53c48c184a..07f3460b8c 100644 --- a/libs/labelbox/tests/data/annotation_import/fixtures/annotations.py +++ b/libs/labelbox/tests/data/conftest.py @@ -1,8 +1,8 @@ import pytest + from labelbox.data.annotation_types.classification.classification import Checklist, ClassificationAnnotation, ClassificationAnswer, Radio from labelbox.data.annotation_types.geometry.point import Point from labelbox.data.annotation_types.geometry.rectangle import Rectangle - from labelbox.data.annotation_types.video import VideoObjectAnnotation diff --git a/libs/labelbox/tests/integration/conftest.py b/libs/labelbox/tests/integration/conftest.py index 0fe114614a..0f246aa378 100644 --- a/libs/labelbox/tests/integration/conftest.py +++ b/libs/labelbox/tests/integration/conftest.py @@ -30,545 +30,6 @@ from labelbox import Client -IMG_URL = "https://picsum.photos/200/300.jpg" -MASKABLE_IMG_URL = "https://storage.googleapis.com/labelbox-datasets/image_sample_data/2560px-Kitano_Street_Kobe01s5s4110.jpeg" -SMALL_DATASET_URL = "https://storage.googleapis.com/lb-artifacts-testing-public/sdk_integration_test/potato.jpeg" -DATA_ROW_PROCESSING_WAIT_TIMEOUT_SECONDS = 30 -DATA_ROW_PROCESSING_WAIT_SLEEP_INTERNAL_SECONDS = 3 -EPHEMERAL_BASE_URL = "http://lb-api-public" -IMAGE_URL = "https://storage.googleapis.com/diagnostics-demo-data/coco/COCO_train2014_000000000034.jpg" -EXTERNAL_ID = "my-image" - - -class Environ(Enum): - LOCAL = 'local' - PROD = 'prod' - STAGING = 'staging' - CUSTOM = 'custom' - STAGING_EU = 'staging-eu' - EPHEMERAL = 'ephemeral' # Used for testing PRs with ephemeral environments - - - -@pytest.fixture -def image_url() -> str: - return IMAGE_URL - - -@pytest.fixture -def external_id() -> str: - return EXTERNAL_ID - - -def ephemeral_endpoint() -> str: - return os.getenv('LABELBOX_TEST_BASE_URL', EPHEMERAL_BASE_URL) - - -def graphql_url(environ: str) -> str: - if environ == Environ.PROD: - return 'https://api.labelbox.com/graphql' - elif environ == Environ.STAGING: - return 'https://api.lb-stage.xyz/graphql' - elif environ == Environ.CUSTOM: - graphql_api_endpoint = os.environ.get( - 'LABELBOX_TEST_GRAPHQL_API_ENDPOINT') - if graphql_api_endpoint is None: - raise Exception(f"Missing LABELBOX_TEST_GRAPHQL_API_ENDPOINT") - return graphql_api_endpoint - elif environ == Environ.EPHEMERAL: - return f"{ephemeral_endpoint()}/graphql" - return 'http://host.docker.internal:8080/graphql' - - -def rest_url(environ: str) -> str: - if environ == Environ.PROD: - return 'https://api.labelbox.com/api/v1' - elif environ == Environ.STAGING: - return 'https://api.lb-stage.xyz/api/v1' - elif environ == Environ.CUSTOM: - rest_api_endpoint = os.environ.get('LABELBOX_TEST_REST_API_ENDPOINT') - if rest_api_endpoint is None: - raise Exception(f"Missing LABELBOX_TEST_REST_API_ENDPOINT") - return rest_api_endpoint - elif environ == Environ.EPHEMERAL: - return f"{ephemeral_endpoint()}/api/v1" - return 'http://host.docker.internal:8080/api/v1' - - -def testing_api_key(environ: str) -> str: - for var in [ - "LABELBOX_TEST_API_KEY_PROD", "LABELBOX_TEST_API_KEY_STAGING", - "LABELBOX_TEST_API_KEY_CUSTOM", "LABELBOX_TEST_API_KEY_LOCAL", - "LABELBOX_TEST_API_KEY" - ]: - value = os.environ.get(var) - if value is not None: - return value - raise Exception("Cannot find API to use for tests") - - -def service_api_key() -> str: - service_api_key = os.environ["SERVICE_API_KEY"] - if service_api_key is None: - raise Exception( - "SERVICE_API_KEY is missing and needed for admin client") - return service_api_key - - -class IntegrationClient(Client): - - def __init__(self, environ: str) -> None: - api_url = graphql_url(environ) - api_key = testing_api_key(environ) - rest_endpoint = rest_url(environ) - - super().__init__(api_key, - api_url, - enable_experimental=True, - rest_endpoint=rest_endpoint) - self.queries = [] - - def execute(self, query=None, params=None, check_naming=True, **kwargs): - if check_naming and query is not None: - assert re.match(r"\s*(?:query|mutation) \w+PyApi", - query) is not None - self.queries.append((query, params)) - return super().execute(query, params, **kwargs) - - -class AdminClient(Client): - - def __init__(self, env): - """ - The admin client creates organizations and users using admin api described here https://labelbox.atlassian.net/wiki/spaces/AP/pages/2206564433/Internal+Admin+APIs. - """ - self._api_key = service_api_key() - self._admin_endpoint = f"{ephemeral_endpoint()}/admin/v1" - self._api_url = graphql_url(env) - self._rest_endpoint = rest_url(env) - - super().__init__(self._api_key, - self._api_url, - enable_experimental=True, - rest_endpoint=self._rest_endpoint) - - def _create_organization(self) -> str: - endpoint = f"{self._admin_endpoint}/organizations/" - response = requests.post( - endpoint, - headers=self.headers, - json={"name": f"Test Org {uuid.uuid4()}"}, - ) - - data = response.json() - if response.status_code not in [ - requests.codes.created, requests.codes.ok - ]: - raise Exception("Failed to create org, message: " + - str(data['message'])) - - return data['id'] - - def _create_user(self, organization_id=None) -> Tuple[str, str]: - if organization_id is None: - organization_id = self.organization_id - - endpoint = f"{self._admin_endpoint}/user-identities/" - identity_id = f"e2e+{uuid.uuid4()}" - - response = requests.post( - endpoint, - headers=self.headers, - json={ - "identityId": identity_id, - "email": "email@email.com", - "name": f"tester{uuid.uuid4()}", - "verificationStatus": "VERIFIED", - }, - ) - data = response.json() - if response.status_code not in [ - requests.codes.created, requests.codes.ok - ]: - raise Exception("Failed to create user, message: " + - str(data['message'])) - - user_identity_id = data['identityId'] - - endpoint = f"{self._admin_endpoint}/organizations/{organization_id}/users/" - response = requests.post( - endpoint, - headers=self.headers, - json={ - "identityId": user_identity_id, - "organizationRole": "Admin" - }, - ) - - data = response.json() - if response.status_code not in [ - requests.codes.created, requests.codes.ok - ]: - raise Exception("Failed to create link user to org, message: " + - str(data['message'])) - - user_id = data['id'] - - endpoint = f"{self._admin_endpoint}/users/{user_id}/token" - response = requests.get( - endpoint, - headers=self.headers, - ) - data = response.json() - if response.status_code not in [ - requests.codes.created, requests.codes.ok - ]: - raise Exception("Failed to create ephemeral user, message: " + - str(data['message'])) - - token = data["token"] - - return user_id, token - - def create_api_key_for_user(self) -> str: - organization_id = self._create_organization() - _, user_token = self._create_user(organization_id) - key_name = f"test-key+{uuid.uuid4()}" - query = """mutation CreateApiKeyPyApi($name: String!) { - createApiKey(data: {name: $name}) { - id - jwt - } - } - """ - params = {"name": key_name} - self.headers["Authorization"] = f"Bearer {user_token}" - res = self.execute(query, params, error_log_key="errors") - - return res["createApiKey"]["jwt"] - - -class EphemeralClient(Client): - - def __init__(self, environ=Environ.EPHEMERAL): - self.admin_client = AdminClient(environ) - self.api_key = self.admin_client.create_api_key_for_user() - api_url = graphql_url(environ) - rest_endpoint = rest_url(environ) - - super().__init__(self.api_key, - api_url, - enable_experimental=True, - rest_endpoint=rest_endpoint) - - -@pytest.fixture -def ephmeral_client() -> EphemeralClient: - return EphemeralClient - - -@pytest.fixture -def admin_client() -> AdminClient: - return AdminClient - - -@pytest.fixture -def integration_client() -> IntegrationClient: - return IntegrationClient - - -@pytest.fixture(scope="session") -def environ() -> Environ: - """ - Checks environment variables for LABELBOX_ENVIRON to be - 'prod' or 'staging' - - Make sure to set LABELBOX_TEST_ENVIRON in .github/workflows/python-package.yaml - - """ - try: - return Environ(os.environ['LABELBOX_TEST_ENVIRON']) - except KeyError: - raise Exception(f'Missing LABELBOX_TEST_ENVIRON in: {os.environ}') - - -def cancel_invite(client, invite_id): - """ - Do not use. Only for testing. - """ - query_str = """mutation CancelInvitePyApi($where: WhereUniqueIdInput!) { - cancelInvite(where: $where) {id}}""" - client.execute(query_str, {'where': {'id': invite_id}}, experimental=True) - - -def get_project_invites(client, project_id): - """ - Do not use. Only for testing. - """ - id_param = "projectId" - query_str = """query GetProjectInvitationsPyApi($from: ID, $first: PageSize, $%s: ID!) { - project(where: {id: $%s}) {id - invites(from: $from, first: $first) { nodes { %s - projectInvites { projectId projectRoleName } } nextCursor}}} - """ % (id_param, id_param, query.results_query_part(Invite)) - return PaginatedCollection(client, - query_str, {id_param: project_id}, - ['project', 'invites', 'nodes'], - Invite, - cursor_path=['project', 'invites', 'nextCursor']) - - -def get_invites(client): - """ - Do not use. Only for testing. - """ - query_str = """query GetOrgInvitationsPyApi($from: ID, $first: PageSize) { - organization { id invites(from: $from, first: $first) { - nodes { id createdAt organizationRoleName inviteeEmail } nextCursor }}}""" - invites = PaginatedCollection( - client, - query_str, {}, ['organization', 'invites', 'nodes'], - Invite, - cursor_path=['organization', 'invites', 'nextCursor'], - experimental=True) - return invites - - -@pytest.fixture -def queries(): - return SimpleNamespace(cancel_invite=cancel_invite, - get_project_invites=get_project_invites, - get_invites=get_invites) - - -@pytest.fixture(scope="session") -def admin_client(environ: str): - return AdminClient(environ) - - -@pytest.fixture(scope="session") -def client(environ: str): - if environ == Environ.EPHEMERAL: - return EphemeralClient() - return IntegrationClient(environ) - - -@pytest.fixture(scope="session") -def image_url(client): - return client.upload_data(requests.get(MASKABLE_IMG_URL).content, - content_type="image/jpeg", - filename="image.jpeg", - sign=True) - - -@pytest.fixture(scope="session") -def pdf_url(client): - pdf_url = client.upload_file('tests/assets/loremipsum.pdf') - return {"row_data": {"pdf_url": pdf_url,}, "global_key": str(uuid.uuid4())} - - -@pytest.fixture(scope="session") -def pdf_entity_data_row(client): - pdf_url = client.upload_file( - 'tests/assets/arxiv-pdf_data_99-word-token-pdfs_0801.3483.pdf') - text_layer_url = client.upload_file( - 'tests/assets/arxiv-pdf_data_99-word-token-pdfs_0801.3483-lb-textlayer.json' - ) - - return { - "row_data": { - "pdf_url": pdf_url, - "text_layer_url": text_layer_url - }, - "global_key": str(uuid.uuid4()) - } - - -@pytest.fixture() -def conversation_entity_data_row(client, rand_gen): - return { - "row_data": - "https://storage.googleapis.com/labelbox-developer-testing-assets/conversational_text/1000-conversations/conversation-1.json", - "global_key": - f"https://storage.googleapis.com/labelbox-developer-testing-assets/conversational_text/1000-conversations/conversation-1.json-{rand_gen(str)}", - } - - -@pytest.fixture -def project(client, rand_gen): - project = client.create_project(name=rand_gen(str), - queue_mode=QueueMode.Batch, - media_type=MediaType.Image) - yield project - project.delete() - - -@pytest.fixture -def consensus_project(client, rand_gen): - project = client.create_project(name=rand_gen(str), - quality_mode=QualityMode.Consensus, - queue_mode=QueueMode.Batch, - media_type=MediaType.Image) - yield project - project.delete() - - -@pytest.fixture -def consensus_project_with_batch(consensus_project, initial_dataset, rand_gen, - image_url): - project = consensus_project - dataset = initial_dataset - - data_rows = [] - for _ in range(3): - data_rows.append({ - DataRow.row_data: image_url, - DataRow.global_key: str(uuid.uuid4()) - }) - task = dataset.create_data_rows(data_rows) - task.wait_till_done() - assert task.status == "COMPLETE" - - data_rows = list(dataset.data_rows()) - assert len(data_rows) == 3 - batch = project.create_batch( - rand_gen(str), - data_rows, # sample of data row objects - 5 # priority between 1(Highest) - 5(lowest) - ) - - yield [project, batch, data_rows] - batch.delete() - - -@pytest.fixture -def dataset(client, rand_gen): - dataset = client.create_dataset(name=rand_gen(str)) - yield dataset - dataset.delete() - - -@pytest.fixture(scope='function') -def unique_dataset(client, rand_gen): - dataset = client.create_dataset(name=rand_gen(str)) - yield dataset - dataset.delete() - - -@pytest.fixture -def small_dataset(dataset: Dataset): - task = dataset.create_data_rows([ - { - "row_data": SMALL_DATASET_URL, - "external_id": "my-image" - }, - ] * 2) - task.wait_till_done() - - yield dataset - - -@pytest.fixture -def data_row(dataset, image_url, rand_gen): - global_key = f"global-key-{rand_gen(str)}" - task = dataset.create_data_rows([ - { - "row_data": image_url, - "external_id": "my-image", - "global_key": global_key - }, - ]) - task.wait_till_done() - dr = dataset.data_rows().get_one() - yield dr - dr.delete() - - -@pytest.fixture -def data_row_and_global_key(dataset, image_url, rand_gen): - global_key = f"global-key-{rand_gen(str)}" - task = dataset.create_data_rows([ - { - "row_data": image_url, - "external_id": "my-image", - "global_key": global_key - }, - ]) - task.wait_till_done() - dr = dataset.data_rows().get_one() - yield dr, global_key - dr.delete() - - -# can be used with -# @pytest.mark.parametrize('data_rows', [], indirect=True) -# if omitted, count defaults to 1 -@pytest.fixture -def data_rows(dataset, image_url, request, wait_for_data_row_processing, - client): - count = 1 - if hasattr(request, 'param'): - count = request.param - - datarows = [ - dict(row_data=image_url, global_key=f"global-key-{uuid.uuid4()}") - for _ in range(count) - ] - - task = dataset.create_data_rows(datarows) - task.wait_till_done() - datarows = dataset.data_rows().get_many(count) - for dr in dataset.data_rows(): - wait_for_data_row_processing(client, dr) - - yield datarows - - for datarow in datarows: - datarow.delete() - - -@pytest.fixture -def iframe_url(environ) -> str: - if environ in [Environ.PROD, Environ.LOCAL]: - return 'https://editor.labelbox.com' - elif environ == Environ.STAGING: - return 'https://editor.lb-stage.xyz' - - -@pytest.fixture -def sample_image() -> str: - path_to_video = 'tests/integration/media/sample_image.jpg' - return path_to_video - - -@pytest.fixture -def sample_video() -> str: - path_to_video = 'tests/integration/media/cat.mp4' - return path_to_video - - -@pytest.fixture -def sample_bulk_conversation() -> list: - path_to_conversation = 'tests/integration/media/bulk_conversation.json' - with open(path_to_conversation) as json_file: - conversations = json.load(json_file) - return conversations - - -@pytest.fixture -def organization(client): - # Must have at least one seat open in your org to run these tests - org = client.get_organization() - # Clean up before and after incase this wasn't run for some reason. - for invite in get_invites(client): - if "@labelbox.com" in invite.email: - cancel_invite(client, invite.uid) - yield org - for invite in get_invites(client): - if "@labelbox.com" in invite.email: - cancel_invite(client, invite.uid) - - @pytest.fixture def project_based_user(client, rand_gen): email = rand_gen(str) @@ -605,14 +66,6 @@ def project_pack(client): proj.delete() -@pytest.fixture -def initial_dataset(client, rand_gen): - dataset = client.create_dataset(name=rand_gen(str)) - yield dataset - - dataset.delete() - - @pytest.fixture def project_with_empty_ontology(project): editor = list( @@ -642,126 +95,6 @@ def configured_project(project_with_empty_ontology, initial_dataset, rand_gen, batch.delete() -@pytest.fixture -def configured_project_with_label(client, rand_gen, image_url, project, dataset, - data_row, wait_for_label_processing): - """Project with a connected dataset, having one datarow - Project contains an ontology with 1 bbox tool - Additionally includes a create_label method for any needed extra labels - One label is already created and yielded when using fixture - """ - project._wait_until_data_rows_are_processed( - data_row_ids=[data_row.uid], - wait_processing_max_seconds=DATA_ROW_PROCESSING_WAIT_TIMEOUT_SECONDS, - sleep_interval=DATA_ROW_PROCESSING_WAIT_SLEEP_INTERNAL_SECONDS) - - project.create_batch( - rand_gen(str), - [data_row.uid], # sample of data row objects - 5 # priority between 1(Highest) - 5(lowest) - ) - ontology = _setup_ontology(project) - label = _create_label(project, data_row, ontology, - wait_for_label_processing) - yield [project, dataset, data_row, label] - - for label in project.labels(): - label.delete() - - -@pytest.fixture -def configured_batch_project_with_label(project, dataset, data_row, - wait_for_label_processing): - """Project with a batch having one datarow - Project contains an ontology with 1 bbox tool - Additionally includes a create_label method for any needed extra labels - One label is already created and yielded when using fixture - """ - data_rows = [dr.uid for dr in list(dataset.data_rows())] - project._wait_until_data_rows_are_processed(data_row_ids=data_rows, - sleep_interval=3) - project.create_batch("test-batch", data_rows) - project.data_row_ids = data_rows - - ontology = _setup_ontology(project) - label = _create_label(project, data_row, ontology, - wait_for_label_processing) - - yield [project, dataset, data_row, label] - - for label in project.labels(): - label.delete() - - -@pytest.fixture -def configured_batch_project_with_multiple_datarows(project, dataset, data_rows, - wait_for_label_processing): - """Project with a batch having multiple datarows - Project contains an ontology with 1 bbox tool - Additionally includes a create_label method for any needed extra labels - """ - global_keys = [dr.global_key for dr in data_rows] - - batch_name = f'batch {uuid.uuid4()}' - project.create_batch(batch_name, global_keys=global_keys) - - ontology = _setup_ontology(project) - for datarow in data_rows: - _create_label(project, datarow, ontology, wait_for_label_processing) - - yield [project, dataset, data_rows] - - for label in project.labels(): - label.delete() - - -def _create_label(project, data_row, ontology, wait_for_label_processing): - predictions = [{ - "uuid": str(uuid.uuid4()), - "schemaId": ontology.tools[0].feature_schema_id, - "dataRow": { - "id": data_row.uid - }, - "bbox": { - "top": 20, - "left": 20, - "height": 50, - "width": 50 - } - }] - - def create_label(): - """ Ad-hoc function to create a LabelImport - Creates a LabelImport task which will create a label - """ - upload_task = LabelImport.create_from_objects( - project.client, project.uid, f'label-import-{uuid.uuid4()}', - predictions) - upload_task.wait_until_done(sleep_time_seconds=5) - assert upload_task.state == AnnotationImportState.FINISHED, "Label Import did not finish" - assert len( - upload_task.errors - ) == 0, f"Label Import {upload_task.name} failed with errors {upload_task.errors}" - - project.create_label = create_label - project.create_label() - label = wait_for_label_processing(project)[0] - return label - - -def _setup_ontology(project): - editor = list( - project.client.get_labeling_frontends( - where=LabelingFrontend.name == "editor"))[0] - ontology_builder = OntologyBuilder(tools=[ - Tool(tool=Tool.Type.BBOX, name="test-bbox-class"), - ]) - project.setup(editor, ontology_builder.asdict()) - # TODO: ontology may not be synchronous after setup. remove sleep when api is more consistent - time.sleep(2) - return OntologyBuilder.from_project(project) - - @pytest.fixture def configured_project_with_complex_ontology(client, initial_dataset, rand_gen, image_url): @@ -825,69 +158,6 @@ def configured_project_with_complex_ontology(client, initial_dataset, rand_gen, project.delete() -# NOTE this is nice heuristics, also there is this logic _wait_until_data_rows_are_processed in Project -# in case we still have flakiness in the future, we can use it -@pytest.fixture -def wait_for_data_row_processing(): - """ - Do not use. Only for testing. - - Returns DataRow after waiting for it to finish processing media_attributes. - Some tests, specifically ones that rely on label export, rely on - DataRow be fully processed with media_attributes - """ - - def func(client, data_row, compare_with_prev_media_attrs=False): - """ - added check_updated_at because when a data_row is updated from say - an image to pdf, it already has media_attributes and the loop does - not wait for processing to a pdf - """ - prev_media_attrs = data_row.media_attributes if compare_with_prev_media_attrs else None - data_row_id = data_row.uid - timeout_seconds = 60 - while True: - data_row = client.get_data_row(data_row_id) - if data_row.media_attributes and (prev_media_attrs is None or - prev_media_attrs - != data_row.media_attributes): - return data_row - timeout_seconds -= 2 - if timeout_seconds <= 0: - raise TimeoutError( - f"Timed out waiting for DataRow '{data_row_id}' to finish processing media_attributes" - ) - time.sleep(2) - - return func - - -@pytest.fixture -def wait_for_label_processing(): - """ - Do not use. Only for testing. - - Returns project's labels as a list after waiting for them to finish processing. - If `project.labels()` is called before label is fully processed, - it may return an empty set - """ - - def func(project): - timeout_seconds = 10 - while True: - labels = list(project.labels()) - if len(labels) > 0: - return labels - timeout_seconds -= 2 - if timeout_seconds <= 0: - raise TimeoutError( - f"Timed out waiting for label for project '{project.uid}' to finish processing" - ) - time.sleep(2) - - return func - - @pytest.fixture def ontology(client): ontology_builder = OntologyBuilder( @@ -1042,31 +312,21 @@ def export_v2_test_helpers() -> Type[ExportV2Helpers]: @pytest.fixture -def big_dataset(dataset: Dataset): - task = dataset.create_data_rows([ - { - "row_data": IMAGE_URL, - "external_id": EXTERNAL_ID - }, - ] * 3) - task.wait_till_done() - - yield dataset - - -@pytest.fixture -def big_dataset_data_row_ids(big_dataset: Dataset) -> List[str]: +def big_dataset_data_row_ids(big_dataset: Dataset): yield [dr.uid for dr in list(big_dataset.export_data_rows())] @pytest.fixture(scope='function') -def dataset_with_invalid_data_rows(unique_dataset: Dataset, upload_invalid_data_rows_for_dataset): +def dataset_with_invalid_data_rows(unique_dataset: Dataset, + upload_invalid_data_rows_for_dataset): upload_invalid_data_rows_for_dataset(unique_dataset) yield unique_dataset + @pytest.fixture def upload_invalid_data_rows_for_dataset(): + def _upload_invalid_data_rows_for_dataset(dataset: Dataset): task = dataset.create_data_rows([ { @@ -1075,8 +335,10 @@ def _upload_invalid_data_rows_for_dataset(dataset: Dataset): }, ] * 2) task.wait_till_done() + return _upload_invalid_data_rows_for_dataset + def pytest_configure(): pytest.report = defaultdict(int) diff --git a/libs/labelbox/tests/integration/test_batch.py b/libs/labelbox/tests/integration/test_batch.py index 465538c4d3..d622bf99b8 100644 --- a/libs/labelbox/tests/integration/test_batch.py +++ b/libs/labelbox/tests/integration/test_batch.py @@ -162,10 +162,9 @@ def test_batch_creation_for_data_rows_with_issues( assert len(failed_data_row_ids_set.intersection(invalid_data_rows_set)) == 2 -def test_batch_creation_with_processing_timeout(project: Project, - small_dataset: Dataset, - unique_dataset: Dataset, - upload_invalid_data_rows_for_dataset): +def test_batch_creation_with_processing_timeout( + project: Project, small_dataset: Dataset, unique_dataset: Dataset, + upload_invalid_data_rows_for_dataset): """ Create a batch with zero wait time, this means that the waiting logic will throw exception immediately """ @@ -187,7 +186,8 @@ def test_batch_creation_with_processing_timeout(project: Project, project._wait_processing_max_seconds = stashed_wait_timeout -def test_export_data_rows(project: Project, dataset: Dataset, image_url: str, external_id: str): +def test_export_data_rows(project: Project, dataset: Dataset, image_url: str, + external_id: str): n_data_rows = 2 task = dataset.create_data_rows([ { diff --git a/libs/labelbox/tests/integration/test_delegated_access.py b/libs/labelbox/tests/integration/test_delegated_access.py index 16d9f6d467..3f025a1aba 100644 --- a/libs/labelbox/tests/integration/test_delegated_access.py +++ b/libs/labelbox/tests/integration/test_delegated_access.py @@ -71,9 +71,8 @@ def test_no_integration(client, image_url): assert requests.get(dr.row_data).status_code == 200 ds.delete() -@pytest.mark.skip( - reason="Assumes state of account doesn't have integration" -) + +@pytest.mark.skip(reason="Assumes state of account doesn't have integration") def test_no_default_integration(client): ds = client.create_dataset(name="new_ds") assert ds.iam_integration() is None diff --git a/libs/labelbox/tests/integration/test_ephemeral.py b/libs/labelbox/tests/integration/test_ephemeral.py index 18b3a1cd50..6ebcf61c60 100644 --- a/libs/labelbox/tests/integration/test_ephemeral.py +++ b/libs/labelbox/tests/integration/test_ephemeral.py @@ -2,9 +2,8 @@ import pytest -@pytest.mark.skipif( - not os.environ.get('LABELBOX_TEST_ENVIRON') == 'ephemeral', - reason='This test only runs in EPHEMERAL environment') +@pytest.mark.skipif(not os.environ.get('LABELBOX_TEST_ENVIRON') == 'ephemeral', + reason='This test only runs in EPHEMERAL environment') def test_org_and_user_setup(client, ephmeral_client): assert type(client) == ephmeral_client assert client.admin_client @@ -16,8 +15,7 @@ def test_org_and_user_setup(client, ephmeral_client): assert user -@pytest.mark.skipif( - os.environ.get('LABELBOX_TEST_ENVIRON') == 'ephemeral', - reason='This test does not run in EPHEMERAL environment') +@pytest.mark.skipif(os.environ.get('LABELBOX_TEST_ENVIRON') == 'ephemeral', + reason='This test does not run in EPHEMERAL environment') def test_integration_client(client, integration_client): assert type(client) == integration_client diff --git a/libs/labelbox/tests/integration/test_foundry.py b/libs/labelbox/tests/integration/test_foundry.py index 560026079f..6de60bf4a8 100644 --- a/libs/labelbox/tests/integration/test_foundry.py +++ b/libs/labelbox/tests/integration/test_foundry.py @@ -91,7 +91,7 @@ def test_get_app_with_invalid_id(foundry_client): with pytest.raises(lb.exceptions.ResourceNotFoundError): foundry_client._get_app("invalid-id") - +@pytest.mark.skip(reason="broken") def test_run_foundry_app_with_data_row_id(foundry_client, data_row, app, random_str): data_rows = lb.DataRowIds([data_row.uid]) @@ -102,7 +102,7 @@ def test_run_foundry_app_with_data_row_id(foundry_client, data_row, app, task.wait_till_done() assert task.status == 'COMPLETE' - +@pytest.mark.skip(reason="broken") def test_run_foundry_app_with_global_key(foundry_client, data_row, app, random_str): data_rows = lb.GlobalKeys([data_row.global_key]) @@ -113,7 +113,7 @@ def test_run_foundry_app_with_global_key(foundry_client, data_row, app, task.wait_till_done() assert task.status == 'COMPLETE' - +@pytest.mark.skip(reason="broken") def test_run_foundry_app_returns_model_run_id(foundry_client, data_row, app, random_str): data_rows = lb.GlobalKeys([data_row.global_key]) @@ -125,7 +125,7 @@ def test_run_foundry_app_returns_model_run_id(foundry_client, data_row, app, model_run = foundry_client.client.get_model_run(model_run_id) assert model_run.uid == model_run_id - +@pytest.mark.skip(reason="broken") def test_run_foundry_with_invalid_data_row_id(foundry_client, app, random_str): invalid_datarow_id = 'invalid-global-key' data_rows = lb.GlobalKeys([invalid_datarow_id]) @@ -136,7 +136,7 @@ def test_run_foundry_with_invalid_data_row_id(foundry_client, app, random_str): app_id=app.id) assert invalid_datarow_id in exception.value - +@pytest.mark.skip(reason="broken") def test_run_foundry_with_invalid_global_key(foundry_client, app, random_str): invalid_global_key = 'invalid-global-key' data_rows = lb.GlobalKeys([invalid_global_key]) diff --git a/pyproject.toml b/pyproject.toml index 7668d5d5fb..7a2de03ee0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,9 @@ authors = [ { name = "Labelbox", email = "engineering@labelbox.com" } ] dependencies = [ + "sphinx-multiproject>=1.0.0rc1", + "sphinx>=7.1.2", + "sphinx-rtd-theme>=2.0.0", ] readme = "README.md" requires-python = ">= 3.8" @@ -33,3 +36,6 @@ addopts = "-rP -vvv --reruns 5 --reruns-delay 10 --durations=20 -n auto --cov=la markers = """ slow: marks tests as slow (deselect with '-m "not slow"') """ + +[tool.rye.scripts] +docs = "sphinx-build ./docs ./dist" \ No newline at end of file diff --git a/requirements-dev.lock b/requirements-dev.lock index 6773102587..67971060fc 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -8,11 +8,15 @@ # with-sources: false -e file:libs/labelbox +alabaster==0.7.13 + # via sphinx annotated-types==0.6.0 # via pydantic attrs==23.2.0 # via jsonschema # via referencing +babel==2.14.0 + # via sphinx beautifulsoup4==4.12.3 # via nbconvert bleach==6.1.0 @@ -31,6 +35,9 @@ decopatch==1.4.10 # via pytest-cases defusedxml==0.7.1 # via nbconvert +docutils==0.20.1 + # via sphinx + # via sphinx-rtd-theme exceptiongroup==1.2.0 # via pytest execnet==2.0.2 @@ -47,9 +54,12 @@ googleapis-common-protos==1.63.0 # via google-api-core idna==3.6 # via requests +imagesize==1.4.1 + # via sphinx importlib-metadata==7.0.2 # via jupyter-client # via nbconvert + # via sphinx # via yapf importlib-resources==6.3.1 # via jsonschema @@ -58,6 +68,7 @@ iniconfig==2.0.0 # via pytest jinja2==3.1.3 # via nbconvert + # via sphinx jsonschema==4.21.1 # via nbformat jsonschema-specifications==2023.12.1 @@ -93,6 +104,7 @@ packaging==24.0 # via pytest # via pytest-cases # via pytest-rerunfailures + # via sphinx pandocfilters==1.5.1 # via nbconvert pkgutil-resolve-name==1.3.10 @@ -116,6 +128,7 @@ pydantic-core==2.16.3 # via pydantic pygments==2.17.2 # via nbconvert + # via sphinx pytest==8.1.1 # via pytest-cov # via pytest-rerunfailures @@ -129,6 +142,8 @@ pytest-xdist==3.5.0 python-dateutil==2.8.2 # via jupyter-client # via labelbox +pytz==2024.1 + # via babel pyzmq==25.1.2 # via jupyter-client referencing==0.34.0 @@ -139,6 +154,7 @@ regex==2023.12.25 requests==2.31.0 # via google-api-core # via labelbox + # via sphinx rpds-py==0.18.0 # via jsonschema # via referencing @@ -147,8 +163,29 @@ rsa==4.9 six==1.16.0 # via bleach # via python-dateutil +snowballstemmer==2.2.0 + # via sphinx soupsieve==2.5 # via beautifulsoup4 +sphinx==7.1.2 + # via sphinx-rtd-theme + # via sphinxcontrib-jquery +sphinx-multiproject==1.0.0rc1 +sphinx-rtd-theme==2.0.0 +sphinxcontrib-applehelp==1.0.4 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==2.0.1 + # via sphinx +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.3 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 + # via sphinx strenum==0.4.15 # via labelbox tinycss2==1.2.1 diff --git a/requirements.lock b/requirements.lock index a7dbb01642..3745857dc6 100644 --- a/requirements.lock +++ b/requirements.lock @@ -8,14 +8,21 @@ # with-sources: false -e file:libs/labelbox +alabaster==0.7.13 + # via sphinx annotated-types==0.6.0 # via pydantic +babel==2.14.0 + # via sphinx cachetools==5.3.3 # via google-auth certifi==2024.2.2 # via requests charset-normalizer==3.3.2 # via requests +docutils==0.20.1 + # via sphinx + # via sphinx-rtd-theme geojson==3.1.0 # via labelbox google-api-core==2.17.1 @@ -26,6 +33,16 @@ googleapis-common-protos==1.63.0 # via google-api-core idna==3.6 # via requests +imagesize==1.4.1 + # via sphinx +importlib-metadata==7.1.0 + # via sphinx +jinja2==3.1.3 + # via sphinx +markupsafe==2.1.5 + # via jinja2 +packaging==24.0 + # via sphinx protobuf==4.25.3 # via google-api-core # via googleapis-common-protos @@ -38,15 +55,41 @@ pydantic==2.6.4 # via labelbox pydantic-core==2.16.3 # via pydantic +pygments==2.17.2 + # via sphinx python-dateutil==2.8.2 # via labelbox +pytz==2024.1 + # via babel requests==2.31.0 # via google-api-core # via labelbox + # via sphinx rsa==4.9 # via google-auth six==1.16.0 # via python-dateutil +snowballstemmer==2.2.0 + # via sphinx +sphinx==7.1.2 + # via sphinx-rtd-theme + # via sphinxcontrib-jquery +sphinx-multiproject==1.0.0rc1 +sphinx-rtd-theme==2.0.0 +sphinxcontrib-applehelp==1.0.4 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==2.0.1 + # via sphinx +sphinxcontrib-jquery==4.1 + # via sphinx-rtd-theme +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.3 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 + # via sphinx strenum==0.4.15 # via labelbox tqdm==4.66.2 @@ -57,3 +100,5 @@ typing-extensions==4.10.0 # via pydantic-core urllib3==2.2.1 # via requests +zipp==3.18.1 + # via importlib-metadata