diff --git a/README.md b/README.md index 20d7d04..5ad1474 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ For more information on a command, run `dbt-cloud --help`. For more in | Project | [dbt-cloud project get](#dbt-cloud-project-get) | ✅ | GET `https://{dbt_cloud_host}/api/v3/accounts/{account_id}/projects/{id}/` | | Project | [dbt-cloud project list](#dbt-cloud-project-list) | ✅ | GET `https://{dbt_cloud_host}/api/v3/accounts/{account_id}/projects/` | | Project | [dbt-cloud project update](#dbt-cloud-project-update) | ❌ | POST `https://{dbt_cloud_host}/api/v3/accounts/{account_id}/projects/{id}/` | -| Environment | [dbt-cloud environment create](#dbt-cloud-environment-create) | ❌ | POST `https://{dbt_cloud_host}/api/v3/accounts/{account_id}/environments/` | +| Environment | [dbt-cloud environment create](#dbt-cloud-environment-create) | ✅ | POST `https://{dbt_cloud_host}/api/v3/accounts/{account_id}/environments/` | | Environment | [dbt-cloud environment delete](#dbt-cloud-environment-delete) | ✅ | DELETE `https://{dbt_cloud_host}/api/v3/accounts/{account_id}/environments/{id}/` | | Environment | [dbt-cloud environment get](#dbt-cloud-environment-get) | ✅ | GET `https://{dbt_cloud_host}/api/v3/accounts/{account_id}/environments/{id}/` | | Environment | [dbt-cloud environment list](#dbt-cloud-environment-list) | ✅ | GET `https://{dbt_cloud_host}/api/v3/accounts/{account_id}/environments/` | @@ -165,33 +165,43 @@ dbt-cloud project list [Click to view sample response](tests/data/project_list_response.json) +## dbt-cloud environment create +This command a new dbt Cloud environment in a given project. + +### Usage +```bash +dbt-cloud environment create --account-id 123456 --project-id 123457 --name "My environment" --dbt-version "1.5.0-latest" +``` + +[Click to view sample response](tests/data/environment_create_response.json) + ## dbt-cloud environment delete -This command deletes a dbt Cloud environment in a given account. +This command deletes a dbt Cloud environment in a given project. ### Usage ```bash -dbt-cloud environment delete --environment-id 222062 +dbt-cloud environment delete --account-id 123456 --project-id 123457 --environment-id 40480 ``` [Click to view sample response](tests/data/environment_delete_response.json) ## dbt-cloud environment list -This command retrieves environments in a given account. +This command retrieves environments in a given project. ### Usage ```bash -dbt-cloud environment list --account-id 16182 --limit 1 +dbt-cloud environment list --account-id 123456 --project-id 123457 --limit 1 ``` [Click to view sample response](tests/data/environment_list_response.json) ## dbt-cloud environment get -This command retrieves information about an environment in a given account. +This command retrieves information about an environment in a given project. ### Usage ```bash -dbt-cloud environment get --account-id 54321 --environment-id 67890 +dbt-cloud environment get --account-id 123456 --project-id 123457 --environment-id 67890 ``` [Click to view sample response](tests/data/environment_get_response.json) diff --git a/dbt_cloud/cli.py b/dbt_cloud/cli.py index 983bba7..5181227 100644 --- a/dbt_cloud/cli.py +++ b/dbt_cloud/cli.py @@ -22,6 +22,7 @@ DbtCloudProjectDeleteCommand, DbtCloudEnvironmentListCommand, DbtCloudEnvironmentGetCommand, + DbtCloudEnvironmentCreateCommand, DbtCloudEnvironmentDeleteCommand, DbtCloudAccountListCommand, DbtCloudAccountGetCommand, @@ -397,6 +398,13 @@ def get(**kwargs): response = execute_and_print(command) +@environment.command(help=DbtCloudEnvironmentCreateCommand.get_description()) +@DbtCloudEnvironmentCreateCommand.click_options +def create(**kwargs): + command = DbtCloudEnvironmentCreateCommand.from_click_options(**kwargs) + response = execute_and_print(command) + + @environment.command(help=DbtCloudEnvironmentDeleteCommand.get_description()) @DbtCloudEnvironmentDeleteCommand.click_options def delete(**kwargs): diff --git a/dbt_cloud/command/__init__.py b/dbt_cloud/command/__init__.py index 5843eba..c3eee26 100644 --- a/dbt_cloud/command/__init__.py +++ b/dbt_cloud/command/__init__.py @@ -23,6 +23,7 @@ DbtCloudEnvironmentListCommand, DbtCloudEnvironmentGetCommand, DbtCloudEnvironmentDeleteCommand, + DbtCloudEnvironmentCreateCommand, ) from .account import DbtCloudAccountListCommand, DbtCloudAccountGetCommand from .audit_log import DbtCloudAuditLogGetCommand diff --git a/dbt_cloud/command/environment/__init__.py b/dbt_cloud/command/environment/__init__.py index cbf5eba..8827d28 100644 --- a/dbt_cloud/command/environment/__init__.py +++ b/dbt_cloud/command/environment/__init__.py @@ -1,3 +1,4 @@ from .list import DbtCloudEnvironmentListCommand from .get import DbtCloudEnvironmentGetCommand +from .create import DbtCloudEnvironmentCreateCommand from .delete import DbtCloudEnvironmentDeleteCommand diff --git a/dbt_cloud/command/environment/create.py b/dbt_cloud/command/environment/create.py new file mode 100644 index 0000000..6d92583 --- /dev/null +++ b/dbt_cloud/command/environment/create.py @@ -0,0 +1,63 @@ +import requests +from typing import Optional +from pydantic import Field +from dbt_cloud.command.command import DbtCloudProjectCommand +from dbt_cloud.field import DBT_VERSION_FIELD + + +class DbtCloudEnvironmentCreateCommand(DbtCloudProjectCommand): + """Creates a new dbt Cloud environment in a given project.""" + + name: str = Field( + description="Name of the environment.", + ) + id: Optional[int] + connection_id: Optional[int] = Field( + description="Connection ID to use for this environment.", + ) + credentials_id: Optional[int] = Field( + description="Credentials ID to use for this environment.", + ) + created_by_id: Optional[int] = Field( + description="User ID of the user who created this environment.", + ) + dbt_project_subdirectory: Optional[str] = Field( + description="Subdirectory of the dbt project to use for this environment.", + ) + use_custom_branch: bool = Field( + False, + description="Whether to use a custom branch for this environment.", + ) + custom_branch: Optional[str] = Field( + description="Custom branch to use for this environment.", + ) + dbt_version: Optional[str] = DBT_VERSION_FIELD + raw_dbt_version: Optional[str] = Field( + description="Raw dbt version to use for this environment.", + ) + supports_docs: bool = Field( + False, + description="Whether this environment supports docs.", + ) + repository_id: Optional[int] = Field( + description="Repository ID to use for this environment.", + ) + state: int = Field( + 1, + description="State of the environment. 1 = Active.", + ) + custom_environment_variables: Optional[dict] = Field( + description="Custom environment variables to use for this environment.", + ) + + @property + def api_url(self) -> str: + return f"{super().api_url}/environments" + + def execute(self) -> requests.Response: + response = requests.post( + url=self.api_url, + headers=self.request_headers, + json=self.get_payload(exclude_empty=True), + ) + return response diff --git a/dbt_cloud/command/environment/delete.py b/dbt_cloud/command/environment/delete.py index d3a4ba6..3e7606f 100644 --- a/dbt_cloud/command/environment/delete.py +++ b/dbt_cloud/command/environment/delete.py @@ -1,10 +1,10 @@ import requests from pydantic import Field -from dbt_cloud.command.command import DbtCloudAccountCommand +from dbt_cloud.command.command import DbtCloudProjectCommand -class DbtCloudEnvironmentDeleteCommand(DbtCloudAccountCommand): - """Deletes a dbt Cloud environment in a given account.""" +class DbtCloudEnvironmentDeleteCommand(DbtCloudProjectCommand): + """Deletes a dbt Cloud environment in a given project.""" environment_id: int = Field( description="ID of the dbt Cloud environment to delete.", diff --git a/dbt_cloud/command/environment/get.py b/dbt_cloud/command/environment/get.py index fa2381f..e748a33 100644 --- a/dbt_cloud/command/environment/get.py +++ b/dbt_cloud/command/environment/get.py @@ -1,11 +1,10 @@ import requests -from pydantic import PrivateAttr -from dbt_cloud.command.command import DbtCloudAccountCommand +from dbt_cloud.command.command import DbtCloudProjectCommand from dbt_cloud.field import ACCOUNT_ID_FIELD, ENVIRONMENT_ID_FIELD -class DbtCloudEnvironmentGetCommand(DbtCloudAccountCommand): - """Retrieves information about an environment in a given account.""" +class DbtCloudEnvironmentGetCommand(DbtCloudProjectCommand): + """Retrieves information about an environment in a given project.""" environment_id: int = ENVIRONMENT_ID_FIELD account_id: int = ACCOUNT_ID_FIELD diff --git a/dbt_cloud/command/environment/list.py b/dbt_cloud/command/environment/list.py index 3267a4c..6ac99e2 100644 --- a/dbt_cloud/command/environment/list.py +++ b/dbt_cloud/command/environment/list.py @@ -1,12 +1,12 @@ import requests from typing import Optional from pydantic import Field -from dbt_cloud.command.command import DbtCloudAccountCommand +from dbt_cloud.command.command import DbtCloudProjectCommand from dbt_cloud.field import LIMIT_FIELD, OFFSET_FIELD, DBT_VERSION_FIELD -class DbtCloudEnvironmentListCommand(DbtCloudAccountCommand): - """Retrieves environments in a given account.""" +class DbtCloudEnvironmentListCommand(DbtCloudProjectCommand): + """Retrieves environments in a given project.""" limit: Optional[int] = LIMIT_FIELD dbt_version: Optional[str] = DBT_VERSION_FIELD diff --git a/dbt_cloud/field.py b/dbt_cloud/field.py index dbddfae..f52c147 100644 --- a/dbt_cloud/field.py +++ b/dbt_cloud/field.py @@ -60,5 +60,5 @@ def get_env(name: str, default=None, allow_none: bool = False): description="Offset for the returned rows. Must be a positive integer.", ) DBT_VERSION_FIELD = Field( - description="Filter by dbt version (e.g., '1.4.0-latest')", + description="dbt version (e.g., '1.4.0-latest')", ) diff --git a/tests/conftest.py b/tests/conftest.py index 4ffb0c9..1def4b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ DbtCloudEnvironmentListCommand, DbtCloudEnvironmentGetCommand, DbtCloudEnvironmentDeleteCommand, + DbtCloudEnvironmentCreateCommand, DbtCloudAccountListCommand, DbtCloudAccountGetCommand, DbtCloudAuditLogGetCommand, @@ -219,12 +220,27 @@ def load_response(response_name): pytest.param( "environment_delete", DbtCloudEnvironmentDeleteCommand( - api_token=API_TOKEN, account_id=ACCOUNT_ID, environment_id=222062 + api_token=API_TOKEN, + account_id=ACCOUNT_ID, + project_id=PROJECT_ID, + environment_id=222062, ), load_response("environment_delete_response"), "delete", marks=pytest.mark.environment, ), + pytest.param( + "environment_create", + DbtCloudEnvironmentCreateCommand( + api_token=API_TOKEN, + account_id=ACCOUNT_ID, + project_id=PROJECT_ID, + name="pytest environment", + ), + load_response("environment_create_response"), + "post", + marks=pytest.mark.environment, + ), pytest.param( "account_list", DbtCloudAccountListCommand(api_token=API_TOKEN), diff --git a/tests/data/environment_create_response.json b/tests/data/environment_create_response.json new file mode 100644 index 0000000..9e55729 --- /dev/null +++ b/tests/data/environment_create_response.json @@ -0,0 +1,111 @@ +{ + "status": { + "code": 201, + "is_success": true, + "user_message": "Success!", + "developer_message": "" + }, + "data": { + "id": 222601, + "account_id": 123456, + "project_id": 123457, + "credentials_id": null, + "name": "pytest environment", + "dbt_version": "1.6.0-latest", + "raw_dbt_version": "1.6.0-latest", + "type": "deployment", + "use_custom_branch": false, + "custom_branch": null, + "supports_docs": false, + "state": 1, + "created_at": null, + "updated_at": "2023-08-03 11:53:02.868229+00:00", + "project": { + "name": "jaffle_shop", + "account_id": 123456, + "connection_id": 32544, + "repository_id": 40480, + "semantic_layer_config_id": null, + "id": 26597, + "created_at": "2021-04-14 20:23:00.395285+00:00", + "updated_at": "2021-11-16 16:32:43.960836+00:00", + "skipped_setup": false, + "state": 1, + "dbt_project_subdirectory": null, + "connection": { + "id": 32544, + "account_id": 123456, + "project_id": 123457, + "name": "Bigquery", + "type": "bigquery", + "created_by_id": 1321515, + "created_by_service_token_id": null, + "details": { + "project_id": "ANONYMIZED", + "timeout_seconds": 300, + "private_key_id": "ANONYMIZED", + "client_email": "ANONYMIZED", + "client_id": "ANONYMIZED", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/dbt-user%40ANONYMIZED", + "priority": null, + "retries": 1, + "scopes": null, + "location": null, + "maximum_bytes_billed": 0, + "execution_project": null, + "impersonate_service_account": null, + "job_retry_deadline_seconds": 0, + "job_creation_timeout_seconds": 0, + "is_configured_for_oauth": false, + "gcs_bucket": null, + "dataproc_region": null, + "dataproc_cluster_name": null + }, + "state": 1, + "created_at": "2021-11-16 16:26:01.571115+00:00", + "updated_at": "2022-05-18 06:27:34.729528+00:00", + "private_link_endpoint_id": null + }, + "repository": { + "id": 40480, + "account_id": 123456, + "project_id": 123457, + "full_name": "ANONYMIZED", + "remote_url": "ANONYMIZED", + "remote_backend": "github", + "git_clone_strategy": "github_app", + "deploy_key_id": 39445, + "repository_credentials_id": null, + "github_installation_id": 19320059, + "github_webhook_id": null, + "pull_request_url_template": "ANONYMIZED", + "state": 1, + "created_at": "2021-11-16 16:26:24.412439+00:00", + "updated_at": "2021-11-16 16:26:24.412455+00:00", + "deploy_key": { + "id": 39445, + "account_id": 123456, + "state": 1, + "public_key": "ssh-rsa ANONYMIZED" + }, + "github_repo": "ANONYMIZED", + "name": "dbt-cloud-cli", + "git_provider_id": 9437, + "gitlab": null, + "git_provider": null + }, + "group_permissions": null, + "docs_job_id": null, + "freshness_job_id": null, + "docs_job": null, + "freshness_job": null + }, + "jobs": null, + "credentials": null, + "custom_environment_variables": null, + "deployment_type": null + } + } \ No newline at end of file diff --git a/tests/data/environment_delete_response.json b/tests/data/environment_delete_response.json index bf4f34d..aaf1843 100644 --- a/tests/data/environment_delete_response.json +++ b/tests/data/environment_delete_response.json @@ -8,7 +8,7 @@ "data": { "dbt_project_subdirectory": null, "project_id": 247181, - "id": 222062, + "id": 40480, "account_id": 16182, "connection_id": 135120, "repository_id": null, diff --git a/tests/test_cli.py b/tests/test_cli.py index daa2e97..81c7c45 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -37,14 +37,23 @@ def test_cli_account_list_and_get(runner): @pytest.mark.environment @pytest.mark.integration -def test_cli_environment_list_and_get(runner, account_id): +def test_cli_environment_list_and_get(runner, account_id, project_id): # Environment list result = runner.invoke( cli, - ["environment", "list", "--account-id", account_id, "--limit", 2], + [ + "environment", + "list", + "--account-id", + account_id, + "--project-id", + project_id, + "--limit", + 2, + ], ) - assert result.exit_code == 0 + assert result.exit_code == 0, result.output response = json.loads(result.output) environment_id = response["data"][0]["id"] assert len(response["data"]) > 0 @@ -58,6 +67,56 @@ def test_cli_environment_list_and_get(runner, account_id): "get", "--account-id", account_id, + "--project-id", + project_id, + "--environment-id", + environment_id, + ], + ) + + assert result.exit_code == 0 + response = json.loads(result.output) + assert response["data"]["id"] == environment_id + assert response["data"]["account_id"] == account_id + + +@pytest.mark.environment +@pytest.mark.integration +def test_cli_environment_create_and_delete(runner, account_id, project_id): + environment_name = "pytest environment" + + # Environment create + result = runner.invoke( + cli, + [ + "environment", + "create", + "--account-id", + account_id, + "--project-id", + project_id, + "--name", + environment_name, + "--dbt-version", + "1.5.0-latest", + ], + ) + + assert result.exit_code == 0 + response = json.loads(result.output) + + assert response["data"]["name"] == environment_name + assert response["data"]["account_id"] == account_id + + # Environment delete + environment_id = response["data"]["id"] + result = runner.invoke( + cli, + [ + "environment", + "delete", + "--account-id", + account_id, "--environment-id", environment_id, ],