diff --git a/DukeDS/__init__.py b/DukeDS/__init__.py
index c0fc0f2..7ab856f 100644
--- a/DukeDS/__init__.py
+++ b/DukeDS/__init__.py
@@ -2,6 +2,7 @@ from ddsc.sdk.dukeds import DukeDS
 list_projects = DukeDS.list_projects
 create_project = DukeDS.create_project
 delete_project = DukeDS.delete_project
+create_folder = DukeDS.create_folder
 list_files = DukeDS.list_files
 download_file = DukeDS.download_file
 upload_file = DukeDS.upload_file
@@ -11,6 +12,7 @@ can_deliver_to_user_with_email = DukeDS.can_deliver_to_user_with_email
 can_deliver_to_user_with_username = DukeDS.can_deliver_to_user_with_username
 
 __all__ = ['list_projects', 'create_project', 'delete_project',
+           'create_folder',
            'list_files', 'download_file', 'upload_file', 'delete_file',
            'move_file_or_folder',
            'can_deliver_to_user_with_email', 'can_deliver_to_user_with_username']
diff --git a/ddsc/sdk/client.py b/ddsc/sdk/client.py
index 3abe9df..bcbbaa2 100644
--- a/ddsc/sdk/client.py
+++ b/ddsc/sdk/client.py
@@ -333,8 +333,7 @@ class Project(BaseResponseItem):
         :param path: str: path within a project specifying a file or folder to download
         :return: File|Folder
         """
-        child_finder = ChildFinder(path, self)
-        return child_finder.get_child()
+        return ChildFinder.get_child_for_path(self, path)
 
     def try_get_item_for_path(self, path):
         """
@@ -342,12 +341,7 @@ class Project(BaseResponseItem):
         :param path: str: path within a project specifying a file or folder to download
         :return: File|Folder|Project|None
         """
-        try:
-            if path == REMOTE_PATH_SEP:
-                return self
-            return self.get_child_for_path(path)
-        except ItemNotFound:
-            return None
+        return ChildFinder.try_get_item_for_path(self, path)
 
     def create_folder(self, folder_name):
         """
@@ -471,6 +465,22 @@ class Folder(BaseResponseItem):
         """
         return self.dds_connection.create_folder(folder_name, KindType.folder_str, self.id)
 
+    def get_child_for_path(self, path):
+        """
+        Based on a remote path get a single remote child. When not found raises ItemNotFound.
+        :param path: str: path within a project specifying a file or folder to download
+        :return: File|Folder
+        """
+        return ChildFinder.get_child_for_path(self, path)
+
+    def try_get_item_for_path(self, path):
+        """
+        Based on a remote path get a single remote child. When not found returns None.
+        :param path: str: path within a project specifying a file or folder to download
+        :return: File|Folder|Project|None
+        """
+        return ChildFinder.try_get_item_for_path(self, path)
+
     def upload_file(self, local_path, remote_filename=None):
         """
         Upload a new file based on a file on the file system as a top level child of this folder.
@@ -626,13 +636,13 @@ class ChildFinder(object):
     """
     Recursively looks for a child based on a path
     """
-    def __init__(self, remote_path, node):
+    def __init__(self, node, remote_path):
         """
-        :param remote_path: path under a project in DDSConnection
         :param node: Project|Folder to find children under
+        :param remote_path: path under a project in DDSConnection
         """
-        self.remote_path = remote_path
         self.node = node
+        self.remote_path = remote_path
 
     def get_child(self):
         """
@@ -651,6 +661,30 @@ class ChildFinder(object):
                 return self._get_child_recurse(tail, child)
         raise ItemNotFound("No item at path {}".format(self.remote_path))
 
+    @staticmethod
+    def get_child_for_path(node, path):
+        """
+        Based on a remote path get a single remote child. When not found raises ItemNotFound.
+        :param path: str: path within a project specifying a file or folder to download
+        :return: File|Folder
+        """
+        child_finder = ChildFinder(node, path)
+        return child_finder.get_child()
+
+    @staticmethod
+    def try_get_item_for_path(node, path):
+        """
+        Based on a remote path get a single remote child. When not found returns None.
+        :param path: str: path within a project specifying a file or folder to download
+        :return: File|Folder|Project|None
+        """
+        try:
+            if path == REMOTE_PATH_SEP:
+                return node
+            return ChildFinder.get_child_for_path(node, path)
+        except ItemNotFound:
+            return None
+
 
 class PathToFiles(object):
     def __init__(self):
diff --git a/ddsc/sdk/dukeds.py b/ddsc/sdk/dukeds.py
index 3353cff..56796a0 100644
--- a/ddsc/sdk/dukeds.py
+++ b/ddsc/sdk/dukeds.py
@@ -1,6 +1,6 @@
 from __future__ import absolute_import
 import logging
-from ddsc.sdk.client import Client, FileUpload, PathToFiles, ItemNotFound, DuplicateNameError
+from ddsc.sdk.client import Client, FileUpload, PathToFiles, ItemNotFound, DuplicateNameError, RemotePath
 from ddsc.core.userutil import UserUtil
 
 
@@ -35,6 +35,15 @@ class DukeDS(object):
         """
         Session().delete_project(project_name)
 
+    @staticmethod
+    def create_folder(project_name, remote_path):
+        """
+        Create a folder and any necessary parent folders for the specified path.
+        :param project_name: str: name of the project to create a folder in
+        :param remote_path: str: remote path specifying which folder(s) to create
+        """
+        return Session().create_folder(project_name, remote_path)
+
     @staticmethod
     def list_files(project_name):
         """
@@ -151,6 +160,21 @@ class Session(object):
         project.delete()
         self.clear_project_cache()
 
+    def create_folder(self, project_name, remote_path):
+        """
+        Create a folder and any necessary parent folders for the specified path.
+        :param project_name: str: name of the project to create a folder in
+        :param remote_path: str: remote path specifying which folder(s) to create
+        """
+        project = self._get_project_for_name(project_name)
+        remote_path_parts = RemotePath.split(remote_path)
+        parent = project
+        for path_part in remote_path_parts:
+            parent_folder = parent.try_get_item_for_path(path_part)
+            if not parent_folder:
+                parent_folder = parent.create_folder(path_part)
+            parent = parent_folder
+
     def list_files(self, project_name):
         """
         Return a list of file paths that make up project_name

Add create_folder developer API
Request from user to create a folder using the `DukeDS` developer API.
Example command to create a folder and subfolder for a project named 'mouse':
```
import DukeDS
DukeDS.create_folder('mouse', '/date/results')
```
Currently you can only create a folder indirectly by uploading a file.
User wanted to pre-create the folder to avoid issues related to duplicate folders when uploading files in parallel instances.
diff --git a/ddsc/sdk/tests/test_client.py b/ddsc/sdk/tests/test_client.py
index 1748b77..6d0f475 100644
--- a/ddsc/sdk/tests/test_client.py
+++ b/ddsc/sdk/tests/test_client.py
@@ -1,6 +1,6 @@
 from unittest import TestCase
 from ddsc.sdk.client import Client, DDSConnection, BaseResponseItem, Project, Folder, File, FileDownload, FileUpload, \
-    ChildFinder, PathToFiles, ItemNotFound, ProjectSummary
+    ChildFinder, PathToFiles, ItemNotFound, ProjectSummary, REMOTE_PATH_SEP
 from ddsc.core.util import KindType
 from mock import patch, Mock, call
 
@@ -472,40 +472,14 @@ class TestProject(TestCase):
     def test_get_child_for_path(self, mock_child_finder):
         mock_dds_connection = Mock()
         mock_child = Mock()
-        mock_child_finder.return_value.get_child.return_value = mock_child
+        mock_child_finder.get_child_for_path.return_value = mock_child
 
         project = Project(mock_dds_connection, self.project_dict)
         child = project.get_child_for_path('/data/file1.dat')
 
-        mock_child_finder.assert_called_with('/data/file1.dat', project)
+        mock_child_finder.get_child_for_path.assert_called_with(project, '/data/file1.dat')
         self.assertEqual(child, mock_child)
 
-    @patch('ddsc.sdk.client.ChildFinder')
-    def test_try_get_item_for_path__with_project(self, mock_child_finder):
-        mock_dds_connection = Mock()
-        project = Project(mock_dds_connection, self.project_dict)
-        project.get_child_for_path = Mock()
-        item = project.try_get_item_for_path('/')
-        self.assertEqual(item, project)
-        project.get_child_for_path.assert_not_called()
-
-    def test_try_get_item_for_path__with_child(self):
-        mock_dds_connection = Mock()
-        project = Project(mock_dds_connection, self.project_dict)
-        project.get_child_for_path = Mock()
-        item = project.try_get_item_for_path('/data/file1.dat')
-        self.assertEqual(item, project.get_child_for_path.return_value)
-        project.get_child_for_path.assert_called_with('/data/file1.dat')
-
-    def test_try_get_item_for_path__child_not_found(self):
-        mock_dds_connection = Mock()
-        project = Project(mock_dds_connection, self.project_dict)
-        project.get_child_for_path = Mock()
-        project.get_child_for_path.side_effect = ItemNotFound("Not Found")
-        item = project.try_get_item_for_path('/data/file1.dat')
-        self.assertEqual(item, None)
-        project.get_child_for_path.assert_called_with('/data/file1.dat')
-
     def test_create_folder(self):
         mock_dds_connection = Mock()
         mock_folder = Mock()
@@ -764,7 +738,7 @@ class TestChildFinder(TestCase):
             mock_folder,
             mock_file
         ]
-        child_finder = ChildFinder('data.txt', mock_project)
+        child_finder = ChildFinder(mock_project, 'data.txt')
         found_child = child_finder.get_child()
         self.assertEqual(found_child, mock_file)
 
@@ -780,7 +754,7 @@ class TestChildFinder(TestCase):
         mock_project.get_children.return_value = [
             mock_folder,
         ]
-        child_finder = ChildFinder('results/data.txt', mock_project)
+        child_finder = ChildFinder(mock_project, 'results/data.txt')
         found_child = child_finder.get_child()
         self.assertEqual(found_child, mock_file)
 
@@ -790,10 +764,28 @@ class TestChildFinder(TestCase):
         mock_project.get_children.return_value = [
             mock_folder,
         ]
-        child_finder = ChildFinder('data.txt', mock_project)
+        child_finder = ChildFinder(mock_project, 'data.txt')
         with self.assertRaises(ItemNotFound):
             child_finder.get_child()
 
+    def test_try_get_item_for_path_with_slash(self):
+        node = Mock()
+        result = ChildFinder.try_get_item_for_path(node, REMOTE_PATH_SEP)
+        self.assertEqual(result, node)
+
+    @patch('ddsc.sdk.client.ChildFinder.get_child_for_path')
+    def test_try_get_item_for_path_with_child_found(self, mock_get_child_for_path):
+        node = Mock()
+        result = ChildFinder.try_get_item_for_path(node, '/file.txt')
+        self.assertEqual(result, mock_get_child_for_path.return_value)
+
+    @patch('ddsc.sdk.client.ChildFinder.get_child_for_path')
+    def test_try_get_item_for_path_with_child_not_found(self, mock_get_child_for_path):
+        mock_get_child_for_path.side_effect = ItemNotFound()
+        node = Mock()
+        result = ChildFinder.try_get_item_for_path(node, '/file.txt')
+        self.assertEqual(result, None)
+
 
 class TestPathToFiles(TestCase):
     def test_path_creation(self):
diff --git a/ddsc/sdk/tests/test_dukeds.py b/ddsc/sdk/tests/test_dukeds.py
index 91fd6a0..97fa356 100644
--- a/ddsc/sdk/tests/test_dukeds.py
+++ b/ddsc/sdk/tests/test_dukeds.py
@@ -205,6 +205,19 @@ class TestDukeDS(TestCase):
         DukeDS.move_file_or_folder('myproject', '/data/file1.txt', '/data/file1_bak.txt')
         mock_project.move_file_or_folder.assert_called_with('/data/file1.txt', '/data/file1_bak.txt')
 
+    @patch('ddsc.sdk.dukeds.Client')
+    def test_create_folder(self, mock_client):
+        mock_project = Mock()
+        mock_project.try_get_item_for_path.return_value = None
+        mock_project.create_folder.return_value.try_get_item_for_path.return_value = None
+        mock_project.name = 'myproject'
+        mock_client.return_value.get_projects.return_value = [
+            mock_project
+        ]
+        DukeDS.create_folder('myproject', '/data/raw')
+        mock_project.create_folder.assert_called_with("data")
+        mock_project.create_folder.return_value.create_folder.assert_called_with("raw")
+
 
 class TestSession(TestCase):
     @patch('ddsc.sdk.dukeds.Client', autospec=True)

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.7
requirements.txt devRequirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
-e git+https://github.com/Duke-GCB/DukeDSClient.git@397c28fd29ddfbb4c0e66d36ab34c7390ee9fb35#egg=DukeDSClient
exceptiongroup==1.2.2
flake8==3.3.0
future==0.16.0
idna==3.10
importlib-metadata==6.7.0
iniconfig==2.0.0
mccabe==0.6.1
mock==2.0.0
nose==1.3.7
packaging==24.0
pbr==6.1.1
pluggy==1.2.0
pycodestyle==2.3.1
pyflakes==1.5.0
pytest==7.4.4
pytz==2025.2
PyYAML==5.1
requests==2.31.0
six==1.10.0
tenacity==5.0.4
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
zipp==3.15.0

name: DukeDSClient
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==3.4.1
      - exceptiongroup==1.2.2
      - flake8==3.3.0
      - future==0.16.0
      - idna==3.10
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - mccabe==0.6.1
      - mock==2.0.0
      - nose==1.3.7
      - packaging==24.0
      - pbr==6.1.1
      - pluggy==1.2.0
      - pycodestyle==2.3.1
      - pyflakes==1.5.0
      - pytest==7.4.4
      - pytz==2025.2
      - pyyaml==5.1
      - requests==2.31.0
      - six==1.10.0
      - tenacity==5.0.4
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - zipp==3.15.0
prefix: /opt/conda/envs/DukeDSClient

ddsc/sdk/tests/test_client.py::TestProject::test_get_child_for_path ddsc/sdk/tests/test_client.py::TestChildFinder::test_child_not_found ddsc/sdk/tests/test_client.py::TestChildFinder::test_direct_children ddsc/sdk/tests/test_client.py::TestChildFinder::test_grand_children ddsc/sdk/tests/test_client.py::TestChildFinder::test_try_get_item_for_path_with_child_found ddsc/sdk/tests/test_client.py::TestChildFinder::test_try_get_item_for_path_with_child_not_found ddsc/sdk/tests/test_client.py::TestChildFinder::test_try_get_item_for_path_with_slash ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_create_folder
ddsc/sdk/tests/test_client.py::TestClient::test_constructor_default_config ddsc/sdk/tests/test_client.py::TestClient::test_constructor_specify_config ddsc/sdk/tests/test_client.py::TestClient::test_create_project ddsc/sdk/tests/test_client.py::TestClient::test_get_file_by_id ddsc/sdk/tests/test_client.py::TestClient::test_get_folder_by_id ddsc/sdk/tests/test_client.py::TestClient::test_get_project_by_id ddsc/sdk/tests/test_client.py::TestClient::test_get_project_by_name__find_one ddsc/sdk/tests/test_client.py::TestClient::test_get_project_by_name__multiple_found ddsc/sdk/tests/test_client.py::TestClient::test_get_project_by_name__none_found ddsc/sdk/tests/test_client.py::TestClient::test_get_projects ddsc/sdk/tests/test_client.py::TestDDSConnection::test_create_folder ddsc/sdk/tests/test_client.py::TestDDSConnection::test_create_project ddsc/sdk/tests/test_client.py::TestDDSConnection::test_delete_file ddsc/sdk/tests/test_client.py::TestDDSConnection::test_delete_folder ddsc/sdk/tests/test_client.py::TestDDSConnection::test_delete_project ddsc/sdk/tests/test_client.py::TestDDSConnection::test_get_file_by_id ddsc/sdk/tests/test_client.py::TestDDSConnection::test_get_file_download ddsc/sdk/tests/test_client.py::TestDDSConnection::test_get_file_url_dict ddsc/sdk/tests/test_client.py::TestDDSConnection::test_get_folder_by_id ddsc/sdk/tests/test_client.py::TestDDSConnection::test_get_folder_children ddsc/sdk/tests/test_client.py::TestDDSConnection::test_get_project_by_id ddsc/sdk/tests/test_client.py::TestDDSConnection::test_get_project_children ddsc/sdk/tests/test_client.py::TestDDSConnection::test_get_project_files_generator ddsc/sdk/tests/test_client.py::TestDDSConnection::test_get_projects ddsc/sdk/tests/test_client.py::TestDDSConnection::test_move_file ddsc/sdk/tests/test_client.py::TestDDSConnection::test_move_folder ddsc/sdk/tests/test_client.py::TestDDSConnection::test_rename_file ddsc/sdk/tests/test_client.py::TestDDSConnection::test_rename_folder ddsc/sdk/tests/test_client.py::TestDDSConnection::test_upload_file ddsc/sdk/tests/test_client.py::TestBaseResponseItem::test_get_attr ddsc/sdk/tests/test_client.py::TestProject::test_create_folder ddsc/sdk/tests/test_client.py::TestProject::test_delete ddsc/sdk/tests/test_client.py::TestProject::test_get_children ddsc/sdk/tests/test_client.py::TestProject::test_get_summary ddsc/sdk/tests/test_client.py::TestProject::test_move_file_or_folder ddsc/sdk/tests/test_client.py::TestProject::test_portal_url ddsc/sdk/tests/test_client.py::TestProject::test_upload_file ddsc/sdk/tests/test_client.py::TestFolder::test_change_parent ddsc/sdk/tests/test_client.py::TestFolder::test_create_folder ddsc/sdk/tests/test_client.py::TestFolder::test_get_children ddsc/sdk/tests/test_client.py::TestFolder::test_rename ddsc/sdk/tests/test_client.py::TestFolder::test_upload_file ddsc/sdk/tests/test_client.py::TestFile::test_change_parent ddsc/sdk/tests/test_client.py::TestFile::test_current_size ddsc/sdk/tests/test_client.py::TestFile::test_delete ddsc/sdk/tests/test_client.py::TestFile::test_download_to_path ddsc/sdk/tests/test_client.py::TestFile::test_rename ddsc/sdk/tests/test_client.py::TestFile::test_upload_new_version ddsc/sdk/tests/test_client.py::TestFileDownload::test_save_to_path ddsc/sdk/tests/test_client.py::TestFileUpload::test_run_create_folder_new_file ddsc/sdk/tests/test_client.py::TestFileUpload::test_run_existing_file_no_parent_folder ddsc/sdk/tests/test_client.py::TestPathToFiles::test_path_creation ddsc/sdk/tests/test_client.py::TestProjectSummary::test_constructor ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_can_deliver_to_user_with_email ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_can_deliver_to_user_with_email_with_logging_func ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_can_deliver_to_user_with_username ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_can_deliver_to_user_with_username_with_logging_func ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_create_project ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_create_project_raises_for_duplicate_project_name ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_delete_file ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_delete_project ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_delete_project_not_found ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_download_file ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_list_files ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_list_projects ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_move_file_or_folder ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_upload_file ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_upload_file_creating_project_and_parent_dir ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_upload_file_default_dest_is_local_basename ddsc/sdk/tests/test_dukeds.py::TestDukeDS::test_upload_file_makes_new_version ddsc/sdk/tests/test_dukeds.py::TestSession::test_constructor_default_args ddsc/sdk/tests/test_dukeds.py::TestSession::test_constructor_passing_config
ddsc/sdk/dukeds.py DukeDS/__init__.py ddsc/sdk/client.py
ddsc/sdk/dukeds.py DukeDS/__init__.py ddsc/sdk/client.py