diff --git a/.gitignore b/.gitignore
index c3b06df8c7fd07fed9c5c001b4563673fa614471..b5df72d80a4c50f372b4c94c7060bddfde55feed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,7 +8,6 @@ datastore_server/source/core-plugins
 openbis/source/core-plugins
 */bin/*
 **/.DS_Store
-*__pycache__/
 *$py.class
 **/.pydevproject
 **/.shredder
diff --git a/pybis/src/python/.gitignore b/pybis/src/python/.gitignore
index ac37d09509d50a26e266d8690a4fc0a3a67cd0d9..750c2aaaf7232dbd6614ad0a948fe1667767d521 100644
--- a/pybis/src/python/.gitignore
+++ b/pybis/src/python/.gitignore
@@ -5,3 +5,5 @@
 dist
 deleteDatasetsAlreadyDeletedFromApplicationServerTaskLastSeen
 notebooks
+*__pycache__/
+*.mypy_cache/
diff --git a/pybis/src/python/pybis/dataset.py b/pybis/src/python/pybis/dataset.py
index b9dbcbab64d007d6ca52d15bff843ffce9c630ad..9325773d69b25c039208f43410df66f43cf003d6 100644
--- a/pybis/src/python/pybis/dataset.py
+++ b/pybis/src/python/pybis/dataset.py
@@ -1,6 +1,9 @@
 import os
+from functools import partialmethod
+from pathlib import Path
 from threading import Thread
 from queue import Queue
+from typing import Set, Optional, List
 from tabulate import tabulate
 from .openbis_object import OpenBisObject 
 from .definitions import openbis_definitions, get_type_for_entity, get_fetchoption_for_entity
@@ -121,7 +124,7 @@ class DataSet(
             'add_attachment()', 'get_attachments()', 'download_attachments()',
             "get_files()", 'file_list',
             'download()', 
-            'archive()', 'unarchive()' 
+            'archive()', 'unarchive()', 'sftp_source_dir', 'sftp_source_absolute_path', 'symlink_to()','is_symlink()','is_physical()' 
         ] + super().__dir__()
 
     def __setattr__(self, name, value):
@@ -163,9 +166,72 @@ class DataSet(
         except Exception:
             return None
     @property
-    def sftp_path(self):
+    def sftp_source_dir(self):
         return os.path.join(self.experiment.identifier[1:], self.permId)
 
+
+    @property
+    def sftp_source_absolute_path(self):
+        # join mountpoint and source_dir
+        # if source_dir is absolute one has to remove "/" to be able to join it with mountpoint
+        # so far did not find way to achieve this in case they are pathlib.Path
+        if not self.openbis.is_mounted():
+            raise ValueError("Not mounted.")
+
+        source_dir = self.sftp_source_dir
+        if source_dir[0] == "/":
+            source_dir = source_dir[1:]
+        return os.path.join(self.openbis.mountpoint, source_dir)
+
+
+    def symlink_to(self, target_dir: str, replace_if_symlink_exists: bool = True):
+        # replace_if_symlink_exists will replace the the target_dir in case it is an existing symlink
+
+        target_dir_path = Path(target_dir)
+        if target_dir_path.is_symlink() and replace_if_symlink_exists:
+            target_dir_path.unlink()
+
+        target_dir_path.symlink_to(self.sftp_source_absolute_path, target_is_directory=True)
+
+
+    @staticmethod
+    def _file_set(target_dir: str) -> Set[str]:
+        target_dir_path = Path(target_dir)
+        return set(
+            str(el.relative_to(target_dir_path))
+            for el in target_dir_path.glob("**/*")
+            if el.is_file()
+        )
+
+
+    def _is_symlink_or_physical(
+        self, target_dir: str, what: str, expected_file_list: Optional[List[str]] = None,
+    ):
+        target_dir_path = Path(target_dir)
+
+        target_file_set = self._file_set(target_dir)
+
+        if expected_file_list is None:
+            source_file_set = set(self.file_list)
+        else:
+            source_file_set = set(expected_file_list)
+
+        res = source_file_set.issubset(target_file_set)
+        if not res:
+            return res
+        elif what == "symlink":
+            return target_dir_path.exists() and target_dir_path.is_symlink()
+        elif what == "physical":
+            return target_dir_path.exists() and not target_dir_path.is_symlink()
+        else:
+            raise ValueError("Unexpected error")
+
+
+    is_symlink = partialmethod(
+        _is_symlink_or_physical, what="symlink", expected_file_list=None
+    )
+    is_physical = partialmethod(_is_symlink_or_physical, what="physical")
+
     def archive(self, remove_from_data_store=True):
         fetchopts = {
             "removeFromDataStore": remove_from_data_store,
diff --git a/pybis/src/python/pybis/utils.py b/pybis/src/python/pybis/utils.py
index 8c723056940c3111fe3dd53ba3aa5bd2b0bf42e4..24363d7ae95d3f9855a3c1674936f79f16740992 100644
--- a/pybis/src/python/pybis/utils.py
+++ b/pybis/src/python/pybis/utils.py
@@ -253,50 +253,4 @@ def extract_userId(user):
     elif isinstance(user, dict):
         return user['userId']
     else:
-        return str(user)
-
-
-def check_symlink(target_dir: str) -> Path:
-    # there are several options: basically resolvable symlink, non-resolvable symlink, does not exist at all or exists but it is not a symlink
-
-    target_dir_path = Path(target_dir)
-
-    if target_dir_path.is_symlink() and target_dir_path.exists():
-        return target_dir_path.resolve()
-    elif target_dir_path.is_symlink() and not target_dir_path.exists():
-        raise FileNotFoundError(
-            f"target_dir={target_dir} is non-resolvable symlink. No such file or directory: {target_dir_path.resolve()}"
-        )
-    elif not target_dir_path.is_symlink() and not target_dir_path.exists():
-        raise FileNotFoundError(f"target_dir={target_dir} does not exist")
-    else:
-        raise ValueError(f"target_dir={target_dir} exists but it is not a symlink")
-
-
-def create_symlink(
-    target_dir: str, source_dir: str, mountpoint: str, replace_if_symlink: bool = True
-):
-    # Path(mountpoint,ds.experiment.identifier[1:],ds.permId)
-    # replace_if_symlink will replace the the target_dir in case it is an existing symlink
-
-    mountpoint_path = Path(mountpoint)
-
-    # check mountpoint
-    if not mountpoint_path.is_mount():
-        if not mountpoint_path.exists():
-            raise FileNotFoundError(f"mountpoint={mountpoint} does not exist")
-        else:
-            raise ValueError(f"mountpoint={mountpoint} exists but it is not a mount")
-
-    # join mountpoint and source_dir
-    # if source_dir is absolute we have to remove "/" to be able to join it with mountpoint
-    # I did not find any way to achieve this in case they are pathlib.Path
-    if source_dir[0] == "/":
-        source_dir = source_dir[1:]
-    source_dir_path = Path(mountpoint, source_dir)
-
-    target_dir_path = Path(target_dir)
-    if target_dir_path.is_symlink() and replace_if_symlink:
-        target_dir_path.unlink()
-
-    target_dir_path.symlink_to(source_dir_path, target_is_directory=True)
\ No newline at end of file
+        return str(user)
\ No newline at end of file
diff --git a/pybis/src/python/setup.py b/pybis/src/python/setup.py
index 2f9445da8d0d84d767e29137e4d175398dfd5001..ab75d770192f318243be04f70c57408fc1263940 100644
--- a/pybis/src/python/setup.py
+++ b/pybis/src/python/setup.py
@@ -28,7 +28,7 @@ setup(
         'texttable',
         'tabulate',
     ],
-    python_requires=">=3.3",
+    python_requires=">=3.6",
     classifiers=[
         "Programming Language :: Python :: 3",
         "License :: OSI Approved :: Apache Software License",