Repository: lightly-ai/lightly

Issue:
embedding upload not working with new_dataset_name
When using lightly-magic or lightly-upload to upload embedding to a new dataset with `new_dataset_name`, the download of the already existing embeddings (none are there) fails:

```
(venv) user@guest lightly % lightly-magic input_dir=/Users/datasets/clothing-dataset-small/test/dress trainer.max_epochs=0 token=TOKEN new_dataset_name="blub"
########## Starting to embed your dataset.
Compute efficiency: 0.24: 100%|█████| 1/1 [00:09<00:00,  9.04s/it]
Embeddings are stored at /Users/GitHub/lightly/lightly_outputs/2021-11-15/16-03-22/embeddings.csv
########## Starting to upload your dataset to the Lightly platform.
Uploading images (with 12 workers).
 100%|███████▏| 15/15 [00:01<00:00, 13.48imgs/s]
 Finished the upload of the dataset.
Starting upload of embeddings.
Traceback (most recent call last):
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/cli/lightly_cli.py", line 87, in lightly_cli
    return _lightly_cli(cfg)
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/cli/lightly_cli.py", line 38, in _lightly_cli
    _upload_cli(cfg)
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/cli/upload_cli.py", line 101, in _upload_cli
    embeddings = api_workflow_client.embeddings_api \
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/openapi_generated/swagger_client/api/embeddings_api.py", line 55, in get_embeddings_by_dataset_id
    (data) = self.get_embeddings_by_dataset_id_with_http_info(dataset_id, **kwargs)  # noqa: E501
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/openapi_generated/swagger_client/api/embeddings_api.py", line 115, in get_embeddings_by_dataset_id_with_http_info
    return self.api_client.call_api(
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/openapi_generated/swagger_client/api_client.py", line 326, in call_api
    return self.__call_api(resource_path, method,
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/openapi_generated/swagger_client/api_client.py", line 158, in __call_api
    response_data = self.request(
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/openapi_generated/swagger_client/api_client.py", line 348, in request
    return self.rest_client.GET(url,
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/openapi_generated/swagger_client/rest.py", line 234, in GET
    return self.request("GET", url,
  File "/Users/GitHub/lightly/venv/lib/python3.9/site-packages/lightly/openapi_generated/swagger_client/rest.py", line 228, in request
    raise ApiException(http_resp=r)
lightly.openapi_generated.swagger_client.rest.ApiException: (400)
Reason: Bad Request
HTTP response headers: HTTPHeaderDict({'Content-Type': 'application/json; charset=utf-8', 'x-cloud-trace-context': '3aa5b5da3e128a2a549edae34d0156cf/10076201567220533420;o=1', 'Vary': 'Origin, Accept-Encoding', 'Content-Security-Policy': "script-src https:/*.lightly.ai https:/*.youtube.com https:/*.gstatic.com https:/*.google.com https:/*.doubleclick.net https:/*.google-analytics.com https:/*.googletagmanager.com https:/*.fullstory.com;img-src *;default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';object-src 'none';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests", 'X-DNS-Prefetch-Control': 'off', 'Expect-CT': 'max-age=0', 'X-Frame-Options': 'SAMEORIGIN', 'Strict-Transport-Security': 'max-age=15552000; includeSubDomains', 'X-Download-Options': 'noopen', 'X-Content-Type-Options': 'nosniff', 'X-Permitted-Cross-Domain-Policies': 'none', 'Origin-Agent-Cluster': '?1', 'X-XSS-Protection': '0', 'Cross-Origin-Opener-Policy': 'same-origin-allow-popups', 'Cross-Origin-Resource-Policy': 'same-site', 'ETag': 'W/"1d-RxIXLBAjGcBSG7XheTm9uqfrCcY"', 'Date': 'Mon, 15 Nov 2021 15:03:35 GMT', 'Server': 'Google Frontend', 'Content-Length': '29'})
HTTP response body: {
    "code": "BAD_REQUEST"
}

```

The same error occurs when using a non-exiting dataset id.


Discussion:

Resolving patch:
diff --git a/lightly/api/api_workflow_upload_embeddings.py b/lightly/api/api_workflow_upload_embeddings.py
--- a/lightly/api/api_workflow_upload_embeddings.py
+++ b/lightly/api/api_workflow_upload_embeddings.py
@@ -14,19 +14,20 @@
 from lightly.utils.io import check_filenames
 
 
-def _get_csv_reader_from_read_url(read_url: str):
-    """Makes a get request to the signed read url and returns the .csv file.
 
-    """
-    request = Request(read_url, method='GET')
-    with urlopen(request) as response:
-        buffer = io.StringIO(response.read().decode('utf-8'))
-        reader = csv.reader(buffer)
 
-    return reader
+class _UploadEmbeddingsMixin:
 
+    def _get_csv_reader_from_read_url(self, read_url: str):
+        """Makes a get request to the signed read url and returns the .csv file.
 
-class _UploadEmbeddingsMixin:
+        """
+        request = Request(read_url, method='GET')
+        with urlopen(request) as response:
+            buffer = io.StringIO(response.read().decode('utf-8'))
+            reader = csv.reader(buffer)
+
+        return reader
 
 
     def set_embedding_id_by_name(self, embedding_name: str = None):
@@ -141,7 +142,7 @@ def append_embeddings(self,
         # read embedding from API
         embedding_read_url = self.embeddings_api \
             .get_embeddings_csv_read_url_by_id(self.dataset_id, embedding_id)
-        embedding_reader = _get_csv_reader_from_read_url(embedding_read_url)
+        embedding_reader = self._get_csv_reader_from_read_url(embedding_read_url)
         rows = list(embedding_reader)
         header, online_rows = rows[0], rows[1:]
 
@@ -152,8 +153,8 @@ def append_embeddings(self,
             if len(local_rows[0]) != len(header):
                 raise RuntimeError(
                     'Column mismatch! Number of columns in local and remote'
-                    f'embeddings files must match but are {len(local_rows[0])}'
-                    f'and {len(header[0])} respectively.'
+                    f' embeddings files must match but are {len(local_rows[0])}'
+                    f' and {len(header)} respectively.'
                 )
 
             local_rows = local_rows[1:]
@@ -193,10 +194,12 @@ def _order_csv_by_filenames(self, path_to_embeddings_csv: str) -> List[str]:
             index_filenames = header_row.index('filenames')
             filenames = [row[index_filenames] for row in rows_without_header]
 
-            if len(filenames) != len(self.filenames_on_server):
+            filenames_on_server = self.filenames_on_server
+
+            if len(filenames) != len(filenames_on_server):
                 raise ValueError(f'There are {len(filenames)} rows in the embedding file, but '
-                                 f'{len(self.filenames_on_server)} filenames/samples on the server.')
-            if set(filenames) != set(self.filenames_on_server):
+                                 f'{len(filenames_on_server)} filenames/samples on the server.')
+            if set(filenames) != set(filenames_on_server):
                 raise ValueError(f'The filenames in the embedding file and '
                                  f'the filenames on the server do not align')
             check_filenames(filenames)
diff --git a/lightly/cli/upload_cli.py b/lightly/cli/upload_cli.py
--- a/lightly/cli/upload_cli.py
+++ b/lightly/cli/upload_cli.py
@@ -40,22 +40,28 @@ def _upload_cli(cfg, is_cli_call=True):
         print_as_warning('Please specify your access token.')
         cli_api_args_wrong = True
 
-    dataset_id_ok = dataset_id and len(dataset_id) > 0
-    new_dataset_name_ok = new_dataset_name and len(new_dataset_name) > 0
-    if new_dataset_name_ok and not dataset_id_ok:
-        api_workflow_client = ApiWorkflowClient(token=token)
-        api_workflow_client.create_dataset(dataset_name=new_dataset_name)
-    elif dataset_id_ok and not new_dataset_name_ok:
-        api_workflow_client = ApiWorkflowClient(
-            token=token,
-            dataset_id=dataset_id
-        )
+    if dataset_id:
+        if new_dataset_name:
+            print_as_warning(
+                'Please specify either the dataset_id of an existing dataset '
+                'or a new_dataset_name, but not both.'
+            )
+            cli_api_args_wrong = True
+        else:
+            api_workflow_client = \
+                ApiWorkflowClient(token=token, dataset_id=dataset_id)
     else:
-        print_as_warning(
-            'Please specify either the dataset_id of an existing dataset or a '
-            'new_dataset_name.'
-        )
-        cli_api_args_wrong = True
+        if new_dataset_name:
+            api_workflow_client = ApiWorkflowClient(token=token)
+            api_workflow_client.create_dataset(dataset_name=new_dataset_name)
+        else:
+            print_as_warning(
+                'Please specify either the dataset_id of an existing dataset '
+                'or a new_dataset_name.')
+            cli_api_args_wrong = True
+    # delete the dataset_id as it might be an empty string
+    # Use api_workflow_client.dataset_id instead
+    del dataset_id
 
     if cli_api_args_wrong:
         print_as_warning('For help, try: lightly-upload --help')
@@ -99,7 +105,7 @@ def _upload_cli(cfg, is_cli_call=True):
         print('Starting upload of embeddings.')
         try:
             embeddings = api_workflow_client.embeddings_api \
-                .get_embeddings_by_dataset_id(dataset_id=dataset_id)
+                .get_embeddings_by_dataset_id(dataset_id=api_workflow_client.dataset_id)
             # use latest embedding first
             embeddings = sorted(
                 embeddings,
@@ -117,24 +123,14 @@ def _upload_cli(cfg, is_cli_call=True):
 
         if embedding is not None:
 
-            filenames_on_server = api_workflow_client.filenames_on_server
-            with open(path_to_embeddings, 'r') as f:
-                # count number of new embedding rows
-                n_rows = -1 + sum(
-                    1 for row in csv.reader(f)
-                    if list(row)[0] not in set(filenames_on_server)
-                )
-
-            if n_rows < len(filenames_on_server): 
-                # more filenames than rows in the embedding file
-                # -> append rows from server
-                print('Appending embeddings from server.')
-                api_workflow_client.append_embeddings(
-                    path_to_embeddings,
-                    embedding.id,
-                )
-                now = datetime.now().strftime('%Y%m%d_%Hh%Mm%Ss')
-                name = f'{name}_{now}'
+            # -> append rows from server
+            print('Appending embeddings from server.')
+            api_workflow_client.append_embeddings(
+                path_to_embeddings,
+                embedding.id,
+            )
+            now = datetime.now().strftime('%Y%m%d_%Hh%Mm%Ss')
+            name = f'{name}_{now}'
 
         api_workflow_client.upload_embeddings(
             path_to_embeddings_csv=path_to_embeddings, name=name
@@ -148,7 +144,7 @@ def _upload_cli(cfg, is_cli_call=True):
             verbose=True
         )
 
-    if new_dataset_name_ok:
+    if new_dataset_name:
         print(f'The dataset_id of the newly created dataset is '
               f'{bcolors.OKBLUE}{api_workflow_client.dataset_id}{bcolors.ENDC}')
 


Test patch:
diff --git a/tests/api_workflow/mocked_api_workflow_client.py b/tests/api_workflow/mocked_api_workflow_client.py
--- a/tests/api_workflow/mocked_api_workflow_client.py
+++ b/tests/api_workflow/mocked_api_workflow_client.py
@@ -1,6 +1,11 @@
+import csv
+import io
 import tempfile
 import unittest
 from io import IOBase
+
+import numpy as np
+
 from lightly.openapi_generated.swagger_client.models.tag_creator import TagCreator
 
 from requests import Response
@@ -38,6 +43,14 @@
 from lightly.openapi_generated.swagger_client.models.write_csv_url_data import WriteCSVUrlData
 
 
+def _check_dataset_id(dataset_id: str):
+    assert isinstance(dataset_id, str)
+    assert len(dataset_id) > 0
+
+
+N_FILES_ON_SERVER = 100
+
+
 class MockedEmbeddingsApi(EmbeddingsApi):
     def __init__(self, api_client):
         EmbeddingsApi.__init__(self, api_client=api_client)
@@ -58,23 +71,28 @@ def __init__(self, api_client):
         ]
 
     def get_embeddings_csv_write_url_by_id(self, dataset_id: str, **kwargs):
+        _check_dataset_id(dataset_id)
         assert isinstance(dataset_id, str)
         response_ = WriteCSVUrlData(signed_write_url="signed_write_url_valid", embedding_id="embedding_id_xyz")
         return response_
 
     def get_embeddings_by_dataset_id(self, dataset_id, **kwargs) -> List[DatasetEmbeddingData]:
+        _check_dataset_id(dataset_id)
         assert isinstance(dataset_id, str)
         return self.embeddings
 
     def trigger2d_embeddings_job(self, body, dataset_id, embedding_id, **kwargs):
+        _check_dataset_id(dataset_id)
         assert isinstance(body, EmbeddingIdTrigger2dEmbeddingsJobBody)
 
     def get_embeddings_csv_read_url_by_id(self, dataset_id, embedding_id, **kwargs):
+        _check_dataset_id(dataset_id)
         return 'https://my-embedding-read-url.com'
 
 
 class MockedSamplingsApi(SamplingsApi):
     def trigger_sampling_by_id(self, body: SamplingCreateRequest, dataset_id, embedding_id, **kwargs):
+        _check_dataset_id(dataset_id)
         assert isinstance(body, SamplingCreateRequest)
         assert isinstance(dataset_id, str)
         assert isinstance(embedding_id, str)
@@ -103,12 +121,14 @@ def get_job_status_by_id(self, job_id, **kwargs):
 
 class MockedTagsApi(TagsApi):
     def create_initial_tag_by_dataset_id(self, body, dataset_id, **kwargs):
+        _check_dataset_id(dataset_id)
         assert isinstance(body, InitialTagCreateRequest)
         assert isinstance(dataset_id, str)
         response_ = CreateEntityResponse(id="xyz")
         return response_
 
     def get_tag_by_tag_id(self, dataset_id, tag_id, **kwargs):
+        _check_dataset_id(dataset_id)
         assert isinstance(dataset_id, str)
         assert isinstance(tag_id, str)
         response_ = TagData(id=tag_id, dataset_id=dataset_id, prev_tag_id="initial-tag", bit_mask_data="0x80bda23e9",
@@ -116,6 +136,7 @@ def get_tag_by_tag_id(self, dataset_id, tag_id, **kwargs):
         return response_
 
     def get_tags_by_dataset_id(self, dataset_id, **kwargs):
+        _check_dataset_id(dataset_id)
         if dataset_id == 'xyz-no-tags':
             return []
         tag_1 = TagData(id='inital_tag_id', dataset_id=dataset_id, prev_tag_id=None,
@@ -136,15 +157,18 @@ def get_tags_by_dataset_id(self, dataset_id, **kwargs):
         return tags
 
     def perform_tag_arithmetics(self, body: TagArithmeticsRequest, dataset_id, **kwargs):
+        _check_dataset_id(dataset_id)
         return TagBitMaskResponse(bit_mask_data="0x2")
 
     def upsize_tags_by_dataset_id(self, body, dataset_id, **kwargs):
+        _check_dataset_id(dataset_id)
         assert body.upsize_tag_creator == TagCreator.USER_PIP
 
 
 class MockedScoresApi(ScoresApi):
     def create_or_update_active_learning_score_by_tag_id(self, body, dataset_id, tag_id, **kwargs) -> \
             CreateEntityResponse:
+        _check_dataset_id(dataset_id)
         if len(body.scores) > 0 and not isinstance(body.scores[0], float):
             raise AttributeError
         response_ = CreateEntityResponse(id="sampled_tag_id_xyz")
@@ -152,12 +176,14 @@ def create_or_update_active_learning_score_by_tag_id(self, body, dataset_id, tag
 
 
 class MockedMappingsApi(MappingsApi):
-    def __init__(self, *args, **kwargs):
-        self.n_samples = 100
+    def __init__(self, samples_api, *args, **kwargs):
+        self.samples_api = samples_api
+        MappingsApi.__init__(self, *args, **kwargs)
+
+        self.n_samples = N_FILES_ON_SERVER
         sample_names = [f'img_{i}.jpg' for i in range(self.n_samples)]
         sample_names.reverse()
         self.sample_names = sample_names
-        MappingsApi.__init__(self, *args, **kwargs)
         
 
     def get_sample_mappings_by_dataset_id(self, dataset_id, field, **kwargs):
@@ -179,20 +205,24 @@ def get_samples_by_dataset_id(self, dataset_id, **kwargs) -> List[SampleData]:
         return samples
 
     def create_sample_by_dataset_id(self, body, dataset_id, **kwargs):
+        _check_dataset_id(dataset_id)
         assert isinstance(body, SampleCreateRequest)
         response_ = CreateEntityResponse(id="xyz")
         self.sample_create_requests.append(body)
         return response_
 
     def get_sample_image_write_url_by_id(self, dataset_id, sample_id, is_thumbnail, **kwargs):
+        _check_dataset_id(dataset_id)
         url = f"{sample_id}_write_url"
         return url
 
     def get_sample_image_read_url_by_id(self, dataset_id, sample_id, type, **kwargs):
+        _check_dataset_id(dataset_id)
         url = f"{sample_id}_write_url"
         return url
 
     def get_sample_image_write_urls_by_id(self, dataset_id, sample_id, **kwargs) -> SampleWriteUrls:
+        _check_dataset_id(dataset_id)
         thumb_url = f"{sample_id}_thumb_write_url"
         full_url = f"{sample_id}_full_write_url"
         ret = SampleWriteUrls(full=full_url, thumb=thumb_url)
@@ -225,12 +255,15 @@ def create_dataset(self, body: DatasetCreateRequest, **kwargs):
         return response_
 
     def get_dataset_by_id(self, dataset_id):
+        _check_dataset_id(dataset_id)
         return next(dataset for dataset in self.default_datasets if dataset_id == dataset.id)
 
     def register_dataset_upload_by_id(self, body, dataset_id):
+        _check_dataset_id(dataset_id)
         return True
 
     def delete_dataset_by_id(self, dataset_id, **kwargs):
+        _check_dataset_id(dataset_id)
         datasets_without_that_id = [dataset for dataset in self.datasets if dataset.id != dataset_id]
         assert len(datasets_without_that_id) == len(self.datasets) - 1
         self.datasets = datasets_without_that_id
@@ -272,6 +305,10 @@ def call_api(self, resource_path, method,
 
 
 class MockedApiWorkflowClient(ApiWorkflowClient):
+
+    embeddings_filename_base = 'img'
+    n_embedding_rows_on_server = N_FILES_ON_SERVER
+
     def __init__(self, *args, **kwargs):
         lightly.api.api_workflow_client.ApiClient = MockedApiClient
         lightly.api.version_checking.VersioningApi = MockedVersioningApi
@@ -281,9 +318,10 @@ def __init__(self, *args, **kwargs):
         self.jobs_api = MockedJobsApi(api_client=self.api_client)
         self.tags_api = MockedTagsApi(api_client=self.api_client)
         self.embeddings_api = MockedEmbeddingsApi(api_client=self.api_client)
-        self.mappings_api = MockedMappingsApi(api_client=self.api_client)
-        self.scores_api = MockedScoresApi(api_client=self.api_client)
         self.samples_api = MockedSamplesApi(api_client=self.api_client)
+        self.mappings_api = MockedMappingsApi(api_client=self.api_client,
+                                              samples_api=self.samples_api)
+        self.scores_api = MockedScoresApi(api_client=self.api_client)
         self.datasets_api = MockedDatasetsApi(api_client=self.api_client)
         self.quota_api = MockedQuotaApi(api_client=self.api_client)
 
@@ -298,7 +336,32 @@ def upload_file_with_signed_url(
         res = Response()
         return res
 
+    def _get_csv_reader_from_read_url(self, read_url: str):
+        n_rows: int = self.n_embedding_rows_on_server
+        n_dims: int = self.n_dims_embeddings_on_server
+
+        rows_csv = [['filenames'] + [f'embeddings_{i}' for i in range(n_dims)] + ['labels']]
+        for i in range(n_rows):
+            row = [f'{self.embeddings_filename_base}_{i}.jpg']
+            for _ in range(n_dims):
+                row.append(np.random.uniform(0, 1))
+            row.append(i)
+            rows_csv.append(row)
+
+        # save the csv rows in a temporary in-memory string file
+        # using a csv writer and then read them as bytes
+        f = tempfile.SpooledTemporaryFile(mode="rw")
+        writer = csv.writer(f)
+        writer.writerows(rows_csv)
+        f.seek(0)
+        buffer = io.StringIO(f.read())
+        reader = csv.reader(buffer)
+
+        return reader
+
 
 class MockedApiWorkflowSetup(unittest.TestCase):
+    EMBEDDINGS_FILENAME_BASE: str = 'sample'
+
     def setUp(self, token="token_xyz",  dataset_id="dataset_id_xyz") -> None:
         self.api_workflow_client = MockedApiWorkflowClient(token=token, dataset_id=dataset_id)
diff --git a/tests/api_workflow/test_api_workflow_upload_dataset.py b/tests/api_workflow/test_api_workflow_upload_dataset.py
--- a/tests/api_workflow/test_api_workflow_upload_dataset.py
+++ b/tests/api_workflow/test_api_workflow_upload_dataset.py
@@ -144,7 +144,7 @@ def failing_upload_sample(*args, **kwargs):
 
     def test_upload_dataset_twice_with_overlap(self):
 
-        all_sample_names = [f'sample_{i}.jpg' for i in range(10)]
+        all_sample_names = [f'img_upload_twice_{i}.jpg' for i in range(10)]
 
         # upload first part of the dataset (sample_0 - sample_6)
         self.create_fake_dataset(sample_names=all_sample_names[:7])
diff --git a/tests/api_workflow/test_api_workflow_upload_embeddings.py b/tests/api_workflow/test_api_workflow_upload_embeddings.py
--- a/tests/api_workflow/test_api_workflow_upload_embeddings.py
+++ b/tests/api_workflow/test_api_workflow_upload_embeddings.py
@@ -9,37 +9,15 @@
 from lightly.utils.io import save_embeddings, load_embeddings, INVALID_FILENAME_CHARACTERS
 
 import lightly
-from tests.api_workflow.mocked_api_workflow_client import MockedApiWorkflowSetup
+from tests.api_workflow.mocked_api_workflow_client import \
+    MockedApiWorkflowSetup, N_FILES_ON_SERVER
 
 
 class TestApiWorkflowUploadEmbeddings(MockedApiWorkflowSetup):
 
-    EMBEDDINGS_FILENAME_BASE: str = 'sample'
-
-    def mock_get_embeddings_from_api(self, read_url, n_rows: int = 10, n_dims: int = 32):
-
-        rows_csv = [['filenames'] + [f'embeddings_{i}' for i in range(n_dims)] + ['labels']]
-        for i in range(n_rows):
-            row = [f'{TestApiWorkflowUploadEmbeddings.EMBEDDINGS_FILENAME_BASE}_{i}.jpg']
-            for _ in range(n_dims):
-                row.append(random.uniform(0, 1))
-            row.append(i)
-            rows_csv.append(row)
-
-        # save the csv rows in a temporary in-memory string file
-        # using a csv writer and then read them as bytes
-        f = tempfile.SpooledTemporaryFile(mode="rw")
-        writer = csv.writer(f)
-        writer.writerows(rows_csv)
-        f.seek(0)
-        buffer = io.StringIO(f.read())
-        reader = csv.reader(buffer)
-
-        return reader
-
-
     def create_fake_embeddings(self,
                                n_data,
+                               n_data_start: int = 0,
                                n_dims: int = 32,
                                special_name_first_sample: bool = False,
                                special_char_in_first_filename: str = None):
@@ -50,7 +28,7 @@ def create_fake_embeddings(self,
             'embeddings.csv'
         )
 
-        self.sample_names = [f'img_{i}.jpg' for i in range(n_data)]
+        self.sample_names = [f'img_{i}.jpg' for i in range(n_data_start, n_data_start + n_data)]
         if special_name_first_sample:
             self.sample_names[0] = "bliblablub"
         if special_char_in_first_filename:
@@ -69,7 +47,9 @@ def t_ester_upload_embedding(self,
                                  n_data,
                                  n_dims: int = 32,
                                  special_name_first_sample: bool = False,
-                                 special_char_in_first_filename: str = None):
+                                 special_char_in_first_filename: str = None,
+                                 name: str = "embedding_xyz"
+                                 ):
 
         self.create_fake_embeddings(
             n_data,
@@ -79,7 +59,8 @@ def t_ester_upload_embedding(self,
         )
 
         # perform the workflow to upload the embeddings
-        self.api_workflow_client.upload_embeddings(path_to_embeddings_csv=self.path_to_embeddings, name="embedding_xyz")
+        self.api_workflow_client.upload_embeddings(path_to_embeddings_csv=self.path_to_embeddings, name=name)
+        self.api_workflow_client.n_dims_embeddings_on_server = n_dims
 
     def test_upload_success(self):
         n_data = len(self.api_workflow_client.mappings_api.sample_names)
@@ -115,6 +96,15 @@ def test_set_embedding_id_failure(self):
         with self.assertRaises(ValueError):
             self.api_workflow_client.set_embedding_id_by_name(embedding_name)
 
+    def test_upload_existing_embedding(self):
+        embeddings = self.api_workflow_client.embeddings_api.\
+            get_embeddings_by_dataset_id(self.api_workflow_client.dataset_id)
+
+        embedding_name = embeddings[0].name
+
+        n_data = len(self.api_workflow_client.mappings_api.sample_names)
+        self.t_ester_upload_embedding(n_data=n_data, name=embedding_name)
+
     def test_set_embedding_id_default(self):
         self.api_workflow_client.set_embedding_id_by_name()
 
@@ -127,7 +117,9 @@ def test_append_embeddings(self):
         # create a new set of embeddings
         self.create_fake_embeddings(10)
 
-        lightly.api.api_workflow_upload_embeddings._get_csv_reader_from_read_url = self.mock_get_embeddings_from_api
+        # mock the embeddings on the server
+        self.api_workflow_client.n_dims_embeddings_on_server = 32
+
         self.api_workflow_client.append_embeddings(
             self.path_to_embeddings,
             'embedding_id_xyz_2',
@@ -135,40 +127,53 @@ def test_append_embeddings(self):
 
 
     def test_append_embeddings_with_overlap(self):
-    
-        # first upload embeddings
-        n_data = len(self.api_workflow_client.mappings_api.sample_names)
-        self.t_ester_upload_embedding(n_data=n_data)
 
-        # create a new set of embeddings overlapping with current embeddings
-        self.create_fake_embeddings(100)
-
-        # the mock embeddings function returns embeddings which overlap with the ones generated above
-        TestApiWorkflowUploadEmbeddings.EMBEDDINGS_FILENAME_BASE: str = 'img'
-        lightly.api.api_workflow_upload_embeddings._get_csv_reader_from_read_url = self.mock_get_embeddings_from_api
+        # mock the embeddings on the server
+        n_data_server = len(self.api_workflow_client.mappings_api.sample_names)
+        self.api_workflow_client.n_dims_embeddings_on_server = 32
+
+        # create new local embeddings overlapping with server embeddings
+        n_data_start_local = n_data_server // 3
+        n_data_local = n_data_server * 2
+        self.create_fake_embeddings(n_data=n_data_local, n_data_start=n_data_start_local)
+
+        """
+        Assumptions:
+            n_data_server = 100
+            n_data_start_local = 33
+            n_data_local = 200
+        
+        Server embeddings file:
+            filenames: 0 ... 99
+            labels: 0 ... 99
+            
+        Local embeddings file:
+            filenames: 33 ... 232
+            labels: 0 ... 0 (all zero)
+            
+        Appended embedding file must thus be:
+            filenames: 0 ... 232
+            labels: 0 ... 32 (from server) + 0 ... 0 (from local)
+        """
+
+        # append the local embeddings to the server embeddings
         self.api_workflow_client.append_embeddings(
             self.path_to_embeddings,
             'embedding_id_xyz_2',
         )
-        TestApiWorkflowUploadEmbeddings.EMBEDDINGS_FILENAME_BASE: str = 'sample'
 
-        # load embeddings
-        _, labels, filenames = load_embeddings(self.path_to_embeddings)
+        # load the new (appended) embeddings
+        _, labels_appended, filenames_appended = \
+            load_embeddings(self.path_to_embeddings)
 
-        # make sure the list of filenames is equal
-        self.assertListEqual(
-            sorted(self.sample_names),
-            sorted(filenames),
-        )
+        # define the expected filenames and labels
+        self.create_fake_embeddings(n_data=n_data_local + n_data_start_local)
+        _, _, filenames_expected = load_embeddings(self.path_to_embeddings)
+        labels_expected = list(range(n_data_start_local)) + [0] * n_data_local
 
-        # make sure that only new embeddings were added
-        # all local labels are 0 (see "create_fake_embeddings") and all online
-        # labels are the line, therefore the labels of the updated embeddings
-        # must be 20 * [0] + [20, 11, 12, ..., 98, 99]
-        self.assertListEqual(
-            sorted(labels),
-            sorted([0] * n_data + [i for i in range(n_data, 100)])
-        )
+        # make sure the list of filenames and labels equal
+        self.assertListEqual(filenames_appended, filenames_expected)
+        self.assertListEqual(labels_appended, labels_expected)
 
 
     def test_append_embeddings_different_shape(self):
@@ -180,8 +185,9 @@ def test_append_embeddings_different_shape(self):
         # create a new set of embeddings
         self.create_fake_embeddings(10, n_dims=16) # default is 32
 
+        self.api_workflow_client.n_dims_embeddings_on_server = 32
+
         with self.assertRaises(RuntimeError):
-            lightly.api.api_workflow_upload_embeddings._get_csv_reader_from_read_url = self.mock_get_embeddings_from_api
             self.api_workflow_client.append_embeddings(
                 self.path_to_embeddings,
                 'embedding_id_xyz_2',
diff --git a/tests/cli/test_cli_magic.py b/tests/cli/test_cli_magic.py
--- a/tests/cli/test_cli_magic.py
+++ b/tests/cli/test_cli_magic.py
@@ -7,12 +7,8 @@
 from hydra.experimental import compose, initialize
 
 import lightly
-from tests.api_workflow.mocked_api_workflow_client import MockedApiWorkflowSetup, MockedApiWorkflowClient
-
-
-# get the mock embeddings function
-from tests.api_workflow.test_api_workflow_upload_embeddings import TestApiWorkflowUploadEmbeddings
-mock_get_embeddings_from_api = TestApiWorkflowUploadEmbeddings().mock_get_embeddings_from_api
+from tests.api_workflow.mocked_api_workflow_client import \
+    MockedApiWorkflowSetup, MockedApiWorkflowClient, N_FILES_ON_SERVER
 
 
 class TestCLIMagic(MockedApiWorkflowSetup):
@@ -61,14 +57,14 @@ def test_parse_cli_string(self):
         assert self.cfg["upload"] == 'thumbnails'
 
     def test_magic_new_dataset_name(self):
-        lightly.api.api_workflow_upload_embeddings._get_csv_reader_from_read_url = mock_get_embeddings_from_api
-        cli_string = "lightly-magic new_dataset_name='xyz-no-tags'"
+        MockedApiWorkflowClient.n_dims_embeddings_on_server = 32
+        cli_string = "lightly-magic new_dataset_name='dataset_name_xyz'"
         self.parse_cli_string(cli_string)
         lightly.cli.lightly_cli(self.cfg)
 
     def test_magic_new_dataset_id(self):
-        lightly.api.api_workflow_upload_embeddings._get_csv_reader_from_read_url = mock_get_embeddings_from_api
-        cli_string = "lightly-magic dataset_id='xyz-no-tags'"
+        MockedApiWorkflowClient.n_dims_embeddings_on_server = 32
+        cli_string = "lightly-magic dataset_id='dataset_id_xyz'"
         self.parse_cli_string(cli_string)
         lightly.cli.lightly_cli(self.cfg)
 
diff --git a/tests/cli/test_cli_upload.py b/tests/cli/test_cli_upload.py
--- a/tests/cli/test_cli_upload.py
+++ b/tests/cli/test_cli_upload.py
@@ -4,11 +4,14 @@
 import json
 import tempfile
 
+import numpy as np
 import torchvision
 from hydra.experimental import compose, initialize
 
 import lightly
-from tests.api_workflow.mocked_api_workflow_client import MockedApiWorkflowSetup, MockedApiWorkflowClient
+from lightly.utils import save_embeddings
+from tests.api_workflow.mocked_api_workflow_client import \
+    MockedApiWorkflowSetup, MockedApiWorkflowClient, N_FILES_ON_SERVER
 
 
 class TestCLIUpload(MockedApiWorkflowSetup):
@@ -22,7 +25,8 @@ def setUp(self):
         with initialize(config_path="../../lightly/cli/config", job_name="test_app"):
             self.cfg = compose(config_name="config", overrides=["token='123'", f"input_dir={self.folder_path}"])
 
-    def create_fake_dataset(self, n_data: int=5):
+
+    def create_fake_dataset(self, n_data: int=5, n_rows_embeddings: int=5, n_dims_embeddings: int = 4):
         self.dataset = torchvision.datasets.FakeData(size=n_data,
                                                      image_size=(3, 32, 32))
 
@@ -46,6 +50,17 @@ def create_fake_dataset(self, n_data: int=5):
         json.dump(coco_json, self.tfile)
         self.tfile.flush()
 
+        # create fake embeddings
+        self.path_to_embeddings = os.path.join(self.folder_path, 'embeddings.csv')
+        sample_names_embeddings = [f'img_{i}.jpg' for i in range(n_rows_embeddings)]
+        labels = [0] * len(sample_names_embeddings)
+        save_embeddings(
+            self.path_to_embeddings,
+            np.random.randn(n_rows_embeddings, n_dims_embeddings),
+            labels,
+            sample_names_embeddings
+        )
+
 
     def parse_cli_string(self, cli_words: str):
         cli_words = cli_words.replace("lightly-upload ", "")
@@ -74,6 +89,43 @@ def test_upload_new_dataset_name(self):
         self.parse_cli_string(cli_string)
         lightly.cli.upload_cli(self.cfg)
 
+    def test_upload_new_dataset_name_and_embeddings(self):
+        """
+        Idea of workflow:
+        We have 80 embedding rows on the server (n_embedding_rows_on_server).
+        We have 100 filenames on the server (N_FILES_ON_SERVER).
+        We have a dataset with 100 samples and 100 rows in the embeddings file.
+        Then we upload the dataset -> the 20 new samples get uploaded,
+        the 80 existing samples are skipped.
+        The 80 embeddings on the server are tried to be added
+        to the local embeddings file, but the local one already contains all
+        these embedding rows. Thus the new file after the appending equals
+        the local file before appending.
+
+        """
+        dims_embeddings_options = [8, 32]
+        n_embedding_rows_on_server = 80
+        for n_dims_embeddings in dims_embeddings_options:
+            for n_dims_embeddings_server in dims_embeddings_options:
+                with self.subTest(
+                        f"test_{n_dims_embeddings}_{n_dims_embeddings_server}"
+                ):
+
+                    self.create_fake_dataset(
+                        n_data=N_FILES_ON_SERVER,
+                        n_rows_embeddings=N_FILES_ON_SERVER,
+                        n_dims_embeddings=n_dims_embeddings
+                    )
+                    MockedApiWorkflowClient.n_embedding_rows_on_server = n_embedding_rows_on_server
+                    MockedApiWorkflowClient.n_dims_embeddings_on_server = n_dims_embeddings_server
+                    cli_string = f"lightly-upload new_dataset_name='new_dataset_name_xyz' embeddings={self.path_to_embeddings}"
+                    self.parse_cli_string(cli_string)
+                    if n_dims_embeddings != n_dims_embeddings_server:
+                        with self.assertRaises(RuntimeError):
+                            lightly.cli.upload_cli(self.cfg)
+                    else:
+                        lightly.cli.upload_cli(self.cfg)
+
     def test_upload_new_dataset_id(self):
         cli_string = "lightly-upload dataset_id='xyz'"
         self.parse_cli_string(cli_string)