From 4e6ea5cc78c6b0c10acfec5451829d9464a94eac Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Oct 2025 17:05:38 -0700 Subject: [PATCH 1/3] updated sdk --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 79c63f6..993d944 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ ] dependencies = [ "cleanlab-tlm~=1.1,>=1.1.14", - "codex-sdk==0.1.0a30", + "codex-sdk==0.1.0a31", "pydantic>=2.0.0, <3", ] From 70c713a493ed1805e1681bdf2279883e8402a4cb Mon Sep 17 00:00:00 2001 From: Angela Date: Mon, 27 Oct 2025 17:18:19 -0700 Subject: [PATCH 2/3] add support for creating project from template --- src/cleanlab_codex/client.py | 18 ++++++++++++++++++ src/cleanlab_codex/project.py | 30 ++++++++++++++++++++++++++++++ tests/test_client.py | 3 +++ 3 files changed, 51 insertions(+) diff --git a/src/cleanlab_codex/client.py b/src/cleanlab_codex/client.py index 5460a99..d75c4a8 100644 --- a/src/cleanlab_codex/client.py +++ b/src/cleanlab_codex/client.py @@ -68,6 +68,24 @@ def create_project(self, name: str, description: Optional[str] = None) -> Projec return Project.create(self._client, self._organization_id, name, description) + def create_project_from_template( + self, + template_project_id: str, + name: str | None = None, + description: str | None = None, + ) -> Project: + """Create a new project from a template. Project will be created in the organization the client is using. + + Args: + template_project_id (str): The ID of the template project to create the project from. + name (str, optional): Optional name for the project. If not provided, the name will be the same as the template project. + description (str, optional): Optional description for the project. If not provided, the description will be the same as the template project. + + Returns: + Project: The created project. + """ + return Project.create_from_template(self._client, self._organization_id, template_project_id, name, description) + def list_organizations(self) -> list[Organization]: """List the organizations the authenticated user is a member of. diff --git a/src/cleanlab_codex/project.py b/src/cleanlab_codex/project.py index 496b587..6394a97 100644 --- a/src/cleanlab_codex/project.py +++ b/src/cleanlab_codex/project.py @@ -118,6 +118,36 @@ def create( return Project(sdk_client, project_id, verify_existence=False) + @classmethod + def create_from_template( + cls, + sdk_client: _Codex, + organization_id: str, + template_project_id: str, + name: str | None = None, + description: str | None = None, + ) -> Project: + """Create a new project from a template. + + Args: + sdk_client (Codex): The Codex SDK client to use to create the project. This client must be authenticated with a user-level API key. + organization_id (str): The ID of the organization to create the project in. + template_project_id (str): The ID of the template project to create the project from. + name (str, optional): Optional name for the project. If not provided, the name will be the same as the template project. + description (str, optional): Optional description for the project. If not provided, the description will be the same as the template project. + + Returns: + Project: The created project. + """ + project_id = sdk_client.projects.create_from_template( + organization_id=organization_id, + template_project_id=template_project_id, + name=name, + description=description, + extra_headers=_AnalyticsMetadata().to_headers(), + ).id + return Project(sdk_client, project_id, verify_existence=False) + def create_access_key( self, name: str, diff --git a/tests/test_client.py b/tests/test_client.py index 4df1871..539edfc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -63,6 +63,7 @@ def test_create_project_without_description( organization_id=FAKE_ORGANIZATION_ID, updated_at=datetime.now(), description=None, + is_template=False, ) client = Client(DUMMY_API_KEY, organization_id=FAKE_ORGANIZATION_ID) project = client.create_project(FAKE_PROJECT_NAME) # no description @@ -126,6 +127,7 @@ def test_create_project(mock_client_from_api_key: MagicMock, default_headers: di organization_id=FAKE_ORGANIZATION_ID, updated_at=datetime.now(), description=FAKE_PROJECT_DESCRIPTION, + is_template=False, ) mock_client_from_api_key.organization_id = FAKE_ORGANIZATION_ID codex = Client(DUMMY_API_KEY, organization_id=FAKE_ORGANIZATION_ID) @@ -151,6 +153,7 @@ def test_get_project(mock_client_from_api_key: MagicMock) -> None: organization_id=FAKE_ORGANIZATION_ID, updated_at=datetime.now(), description=FAKE_PROJECT_DESCRIPTION, + is_template=False, ) project = Client(DUMMY_API_KEY, organization_id=FAKE_ORGANIZATION_ID).get_project(FAKE_PROJECT_ID) From 089a0e408d43112a6670a06b44296a978720b478 Mon Sep 17 00:00:00 2001 From: Angela Date: Tue, 28 Oct 2025 10:49:26 -0700 Subject: [PATCH 3/3] add tests --- CHANGELOG.md | 8 +++++++- src/cleanlab_codex/__about__.py | 2 +- tests/test_client.py | 34 +++++++++++++++++++++++++++++++-- tests/test_project.py | 19 ++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b4b0d5..a2c5add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.32] 2025-10-28 + +- Add `Client.create_project_from_template()` method to create a new project from a template +- Add `Project.create_from_template()` method to create a new project from a template + ## [1.0.31] 2025-10-14 - Add `expert_guardrail_override_explanation` and `log_id` to `ProjectValidateResponse` docstring @@ -145,7 +150,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release of the `cleanlab-codex` client library. -[Unreleased]: https://github.com/cleanlab/cleanlab-codex/compare/v1.0.31...HEAD +[Unreleased]: https://github.com/cleanlab/cleanlab-codex/compare/v1.0.32...HEAD +[1.0.32]: https://github.com/cleanlab/cleanlab-codex/compare/v1.0.31...v1.0.32 [1.0.31]: https://github.com/cleanlab/cleanlab-codex/compare/v1.0.30...v1.0.31 [1.0.30]: https://github.com/cleanlab/cleanlab-codex/compare/v1.0.29...v1.0.30 [1.0.29]: https://github.com/cleanlab/cleanlab-codex/compare/v1.0.28...v1.0.29 diff --git a/src/cleanlab_codex/__about__.py b/src/cleanlab_codex/__about__.py index cfd3e86..0d70354 100644 --- a/src/cleanlab_codex/__about__.py +++ b/src/cleanlab_codex/__about__.py @@ -1,2 +1,2 @@ # SPDX-License-Identifier: MIT -__version__ = "1.0.31" +__version__ = "1.0.32" diff --git a/tests/test_client.py b/tests/test_client.py index 539edfc..6cdc67f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,7 +8,9 @@ from codex import AuthenticationError from codex.types.project_return_schema import Config as ProjectReturnConfig from codex.types.project_return_schema import ProjectReturnSchema -from codex.types.users.myself.user_organizations_schema import Organization as SDKOrganization +from codex.types.users.myself.user_organizations_schema import ( + Organization as SDKOrganization, +) from codex.types.users.myself.user_organizations_schema import UserOrganizationsSchema from cleanlab_codex.client import Client @@ -22,6 +24,7 @@ FAKE_PROJECT_DESCRIPTION = "Test Description" DEFAULT_PROJECT_CONFIG = ProjectConfig() DUMMY_API_KEY = "GP0FzPfA7wYy5L64luII2YaRT2JoSXkae7WEo7dH6Bw" +FAKE_TEMPLATE_PROJECT_ID = str(uuid.uuid4()) def test_client_uses_default_organization(mock_client_from_api_key: MagicMock) -> None: @@ -41,7 +44,9 @@ def test_client_uses_default_organization(mock_client_from_api_key: MagicMock) - assert client.organization_id == default_org_id -def test_client_uses_specified_organization(mock_client_from_api_key: MagicMock) -> None: +def test_client_uses_specified_organization( + mock_client_from_api_key: MagicMock, +) -> None: """Test that client uses specified organization ID""" specified_org_id = "specified-org-id" client = Client(DUMMY_API_KEY, organization_id=specified_org_id) @@ -161,3 +166,28 @@ def test_get_project(mock_client_from_api_key: MagicMock) -> None: assert mock_client_from_api_key.projects.retrieve.call_count == 1 assert mock_client_from_api_key.projects.retrieve.call_args[0][0] == FAKE_PROJECT_ID + + +def test_create_project_from_template(mock_client_from_api_key: MagicMock, default_headers: dict[str, str]) -> None: + mock_client_from_api_key.projects.create_from_template.return_value = ProjectReturnSchema( + id=FAKE_PROJECT_ID, + config=ProjectReturnConfig(), + created_at=datetime.now(), + created_by_user_id=FAKE_USER_ID, + name=FAKE_PROJECT_NAME, + organization_id=FAKE_ORGANIZATION_ID, + updated_at=datetime.now(), + description=FAKE_PROJECT_DESCRIPTION, + is_template=False, + ) + mock_client_from_api_key.organization_id = FAKE_ORGANIZATION_ID + codex = Client(DUMMY_API_KEY, organization_id=FAKE_ORGANIZATION_ID) + project = codex.create_project_from_template(FAKE_TEMPLATE_PROJECT_ID, FAKE_PROJECT_NAME, FAKE_PROJECT_DESCRIPTION) + mock_client_from_api_key.projects.create_from_template.assert_called_once_with( + organization_id=FAKE_ORGANIZATION_ID, + template_project_id=FAKE_TEMPLATE_PROJECT_ID, + name=FAKE_PROJECT_NAME, + description=FAKE_PROJECT_DESCRIPTION, + extra_headers=default_headers, + ) + assert project.id == FAKE_PROJECT_ID diff --git a/tests/test_project.py b/tests/test_project.py index 4087e86..df8e732 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -23,6 +23,7 @@ DEFAULT_PROJECT_CONFIG = Config() DUMMY_ACCESS_KEY = "sk-1-EMOh6UrRo7exTEbEi8_azzACAEdtNiib2LLa1IGo6kA" FAKE_LOG_ID = str(uuid.uuid4()) +FAKE_TEMPLATE_PROJECT_ID = str(uuid.uuid4()) def test_project_validate_with_dict_response( @@ -218,6 +219,24 @@ def test_create_project(mock_client_from_api_key: MagicMock, default_headers: di assert mock_client_from_api_key.projects.retrieve.call_count == 0 +def test_create_project_from_template(mock_client_from_api_key: MagicMock, default_headers: dict[str, str]) -> None: + mock_client_from_api_key.projects.create_from_template.return_value.id = FAKE_PROJECT_ID + mock_client_from_api_key.organization_id = FAKE_ORGANIZATION_ID + project = Project.create_from_template( + mock_client_from_api_key, + FAKE_ORGANIZATION_ID, + FAKE_TEMPLATE_PROJECT_ID, + ) + assert project.id == FAKE_PROJECT_ID + mock_client_from_api_key.projects.create_from_template.assert_called_once_with( + organization_id=FAKE_ORGANIZATION_ID, + template_project_id=FAKE_TEMPLATE_PROJECT_ID, + name=None, + description=None, + extra_headers=default_headers, + ) + + def test_create_access_key(mock_client_from_api_key: MagicMock, default_headers: dict[str, str]) -> None: project = Project(mock_client_from_api_key, FAKE_PROJECT_ID) access_key_name = "Test Access Key"