diff --git a/docs/nextmv/cloud/index.md b/docs/nextmv/cloud/index.md index ea32187..6436ade 100644 --- a/docs/nextmv/cloud/index.md +++ b/docs/nextmv/cloud/index.md @@ -29,31 +29,38 @@ interacting with Nextmv Cloud. ## Application & run management -Tutorials to manage your Cloud Applications and their runs. +Tutorials to manage your Cloud applications and their runs. | Feature | Description | |---------|-------------| -| [Managing an Application][manage] | Create, delete, and list applications | -| [Pushing an Application][push] | Upload your executable decision model to a Cloud Application | -| [Running an Application][runs] | Create and manage runs for your Cloud Application | -| [Versions][versions] | Manage versions of your Cloud Application | -| [Instances][instances] | Manage instances of your Cloud Application | -| [Track Runs][external-runs] | Associate external runs executed outside of Cloud with your Cloud Application | -| [Secrets][secrets] | Manage secrets to use in your Cloud Application | -| [Large Payloads][large-payloads] | Manage large payloads for your Cloud Application | -| [Queuing & prioritization][queuing] | Manage queuing and prioritization for your Cloud Application | -| [Execution classes][execution-classes] | Manage execution classes for your Cloud Application | +| [Managing an application][manage] | Create, delete, and list applications | +| [Pushing an application][push] | Upload your executable decision model to a Cloud application | +| [Running an application][runs] | Create and manage runs for your Cloud application | +| [Versions][versions] | Manage versions of your Cloud application | +| [Instances][instances] | Manage instances of your Cloud application | +| [Track Runs][external-runs] | Associate external runs executed outside of Cloud with your Cloud application | +| [Secrets][secrets] | Manage secrets to use in your Cloud application | +| [Large Payloads][large-payloads] | Manage large payloads for your Cloud application | +| [Queuing & prioritization][queuing] | Manage queuing and prioritization for your Cloud application | +| [Execution classes][execution-classes] | Manage execution classes for your Cloud application | ## Application testing and experimentation -Tutorials to manage testing and experimentation for your Cloud Applications. +Tutorials to manage testing and experimentation for your Cloud applications. | Feature | Description | |---------|-------------| -| [Scenario tests][scenario-tests] | Manage scenario tests for your Cloud Application | -| [Batch experiments][batch-experiments] | Manage batch experiments for your Cloud Application | -| [Acceptance tests][acceptance-tests] | Manage acceptance tests for your Cloud Application | -| [Input sets][input-sets] | Manage input sets for your Cloud Application | +| [Scenario tests][scenario-tests] | Manage scenario tests for your Cloud application | +| [Batch experiments][batch-experiments] | Manage batch experiments for your Cloud application | +| [Acceptance tests][acceptance-tests] | Manage acceptance tests for your Cloud application | +| [Input sets][input-sets] | Manage input sets for your Cloud application | + +## Account management + +| Feature | Description | +|---------|-------------| +| [Queuing & prioritization][queuing] | Get the queue for your Cloud account | +| [Integrations][integrations] | Manage integrations for your Cloud account | [signup]: https://cloud.nextmv.io [api-key]: https://cloud.nextmv.io/team/api-keys @@ -68,6 +75,7 @@ Tutorials to manage testing and experimentation for your Cloud Applications. [large-payloads]: ./large-payloads.md [queuing]: ./queuing.md [execution-classes]: ./execution-classes.md +[integrations]: ./integrations.md [scenario-tests]: ./scenario-tests.md [batch-experiments]: ./batch-experiments.md [acceptance-tests]: ./acceptance-tests.md diff --git a/docs/nextmv/cloud/instances.md b/docs/nextmv/cloud/instances.md index 11ca7b9..f10590c 100644 --- a/docs/nextmv/cloud/instances.md +++ b/docs/nextmv/cloud/instances.md @@ -1,4 +1,4 @@ -# Manage Instances for an Application +# Manage instances for an application !!! tip "Reference" @@ -8,9 +8,9 @@ Application instances allow you to deploy and run different versions of your app environments. This is useful for separating development, staging, and production workloads, or running multiple configurations of the same application. -## Understanding Instances +## Understanding instances -A Nextmv Cloud Application Instance is a way to configure your runs in a repeatable way. +A Nextmv Cloud application instance is a way to configure your runs in a repeatable way. Each instance can run a specific version of your application with its own configuration. When you create an instance, you specify which version of your application run. @@ -18,7 +18,7 @@ When you create an instance, you specify which version of your application run. After creating a [version][version] from the latest push, you can either create a new instance or update an instance with the latest version. -## Creating Instances +## Creating instances If you want to create a new instance after using `app.push()`, your script might include the following. @@ -51,7 +51,7 @@ instance = app.new_instance( ) ``` -## Updating Instances +## Updating instances Often, you will already have an instance created that you might want to update with you new version. In this case, your `push.py` might look like this: diff --git a/docs/nextmv/cloud/integrations.md b/docs/nextmv/cloud/integrations.md new file mode 100644 index 0000000..05650ff --- /dev/null +++ b/docs/nextmv/cloud/integrations.md @@ -0,0 +1,290 @@ +# Manage integrations + +!!! tip "Reference" + + Find the reference for the `Integration` class [here](./reference/integration.md). + +Integrations allow Nextmv Cloud to communicate with external systems or +services. This enables you to connect your applications to runtime environments +like Databricks or data sources for seamless data exchange and execution. + +## Understanding integrations + +A Nextmv Cloud integration is a configuration that establishes a connection +between your Nextmv applications and external platforms. Each integration +specifies: + +* The **provider** (e.g., Databricks) +* The **integration type** (runtime or data) +* Supported **execution types** (e.g., Python) +* Provider-specific **configuration** details + +Integrations can be either **global** (available to all applications in your +account) or **application-specific** (limited to selected applications). + +## Creating integrations + +To create a new integration, use the `Integration.new` class method: + +```python +import os + +from nextmv import cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) + +integration = cloud.Integration.new( + client=client, + name="My Databricks Runtime", + integration_id="my-dbx-runtime", # Auto-generated if omitted + description="Databricks integration for production workloads", + integration_type=cloud.IntegrationType.RUNTIME, + exec_types=[cloud.ManifestType.PYTHON], + provider=cloud.IntegrationProvider.DBX, + provider_config={ + "job_id": 123456789, + "client_id": "a-client-id-123", + "client_secret": "client-secret", + "workspace_url": "https://identifier.cloud.databricks.com", + }, + is_global=True, +) + +print(f"Created integration: {integration.integration_id}") +``` + +If you want to create an integration that's only available to specific applications: + +```python +import os + +from nextmv import cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) + +integration = cloud.Integration.new( + client=client, + name="App-Specific Integration", + integration_id="my-dbx-runtime", # Auto-generated if omitted + integration_type=cloud.IntegrationType.DATA, + exec_types=[cloud.ManifestType.PYTHON], + provider=cloud.IntegrationProvider.DBX, + provider_config={"config_key": "config_value"}, + is_global=False, + application_ids=["app-id-1", "app-id-2"], +) + +print(f"Created integration: {integration.integration_id}") +``` + +The `exist_ok` parameter can be set to `True` to instantiate the integration if +it already exists. + +## Running with an integration + +Once you've created an integration, you can use it to execute your application +runs on external compute resources. There are two ways to configure an +integration for your runs: + +1. Directly in the run configuration. +2. At the instance level. + +### 1. Direct integration in run configuration + +You can specify an integration directly when submitting a run using the +`RunConfiguration` class. This approach is useful for one-off runs or when you +want to use a specific integration regardless of the instance configuration. + +```python +import os + +from nextmv import RunConfiguration, cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) +app = cloud.Application(client=client, id="") + +# Submit a run with a specific integration +run_id = app.new_run( + input={"key": "value"}, + configuration=RunConfiguration(integration_id="my-dbx-runtime"), +) + +print(f"Submitted run: {run_id}") +``` + +To get the results directly, use `new_run_with_result`: + +```python +import os + +from nextmv import RunConfiguration, cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) +app = cloud.Application(client=client, id="") + +# Submit a run and wait for results +result = app.new_run_with_result( + input={"key": "value"}, + configuration=RunConfiguration(integration_id="my-dbx-runtime"), +) + +print(f"Run completed: {result.metadata.status_v2.value}") +print(f"Output: {result.output}") +``` + +!!! note + + When you specify an `integration_id`, the `execution_class` is automatically + set to `"integration"`. You don't need to specify it manually. + +!!! note + + Specifying an integration directly in the run configuration overrides any + integration set at the instance level. This is useful when you need to: + + * Test different integrations without modifying the instance. + * Route specific runs to different compute environments. + * Use a fallback integration for certain scenarios. + +### 2. Instance-level integration + +For more permanent configurations, you can set an integration at the instance +level using `InstanceConfiguration`. When you submit runs using that instance, +the integration will be automatically applied. + +First, create an instance with an integration: + +```python +import os + +from nextmv import cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) +app = cloud.Application(client=client, id="") + +# Create an instance with an integration +instance = app.new_instance( + id="inst_databricks", + name="Databricks Instance", + version_id="ver_1234567890", + configuration=cloud.InstanceConfiguration( + integration_id="my-dbx-runtime", + ), +) + +print(f"Created instance: {instance.id}") +``` + +Then, submit runs using the instance, the integration is automatically applied: + +```python +import os + +from nextmv import cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) +app = cloud.Application(client=client, id="") + +# The integration from the instance configuration is used +run_id = app.new_run( + input={"key": "value"}, + instance_id="inst_databricks", +) + +print(f"Submitted run using instance integration: {run_id}") + +``` + +Or get results directly with the `new_run_with_result` method. + +## Getting integrations + +To retrieve an existing integration and ensure all fields are properly +populated, use the `Integration.get` class method: + +```python +import os + +from nextmv import cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) + +integration = cloud.Integration.get(client=client, integration_id="my-dbx-runtime") + +print(f"Integration name: {integration.name}") +print(f"Provider: {integration.provider}") +print(f"Type: {integration.integration_type}") +print(f"Global: {integration.is_global}") +``` + +To view all integrations available in your Nextmv Cloud account, use the +`list_integrations` function: + +```python +import os + +from nextmv import cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) + +integrations = cloud.list_integrations(client=client) + +for integration in integrations: + print(f"ID: {integration.integration_id}") + print(f"Name: {integration.name}") + print(f"Type: {integration.integration_type}") + print(f"Provider: {integration.provider}") + print(f"Global: {integration.is_global}") + print("---") +``` + +## Updating integrations + +To update an existing integration, call the `update` method with the fields you +want to change: + +```python +import os + +from nextmv import cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) + +integration = cloud.Integration.get(client=client, integration_id="my-dbx-runtime") + +updated_integration = integration.update( + name="Updated Databricks Runtime", + description="Updated configuration for production", + provider_config={ + "workspace_url": "https://new-workspace.databricks.com", + "cluster_id": "new-cluster-id", + "token": "new-token", + }, +) + +print(f"Updated integration: {updated_integration.name}") +``` + +You can update any combination of fields. + +## Deleting integrations + +To delete an integration from Nextmv Cloud, use the `delete` method: + +```python +import os + +from nextmv import cloud + +client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY")) + +integration = cloud.Integration.get(client=client, integration_id="my-dbx-runtime") + +integration.delete() +print("Integration deleted successfully") +``` + +!!! warning + + Deleting an integration is permanent and cannot be undone. Make sure you're + not actively using the integration in any applications before deleting it. diff --git a/docs/nextmv/cloud/push.md b/docs/nextmv/cloud/push.md index 405a6f2..e9b21b1 100644 --- a/docs/nextmv/cloud/push.md +++ b/docs/nextmv/cloud/push.md @@ -1,10 +1,10 @@ -# Push to an Application +# Push to an application !!! tip "Reference" Find the reference for the `Application` class [here](./reference/application.md). -A Nextmv Cloud Application is a decision model that be executed remotely on the +A Nextmv Cloud application is a decision model that be executed remotely on the Nextmv Platform. An application is an executable program that fulfills the following minimum requirements: diff --git a/docs/nextmv/cloud/reference/integration.md b/docs/nextmv/cloud/reference/integration.md new file mode 100644 index 0000000..80f3021 --- /dev/null +++ b/docs/nextmv/cloud/reference/integration.md @@ -0,0 +1,5 @@ +# Integration Module + +This section documents the integration components of the Nextmv Cloud API. + +::: nextmv.nextmv.cloud.integration diff --git a/docs/nextmv/cloud/runs.md b/docs/nextmv/cloud/runs.md index b97016c..2a330b6 100644 --- a/docs/nextmv/cloud/runs.md +++ b/docs/nextmv/cloud/runs.md @@ -1,4 +1,4 @@ -# Run a Cloud Application +# Run a Cloud application !!! tip "Reference" diff --git a/docs/nextmv/cloud/secrets.md b/docs/nextmv/cloud/secrets.md index c483a01..a1b9513 100644 --- a/docs/nextmv/cloud/secrets.md +++ b/docs/nextmv/cloud/secrets.md @@ -18,7 +18,7 @@ documentation][secrets-collections]. ## Create a secrets collection -Start by creating a secrets collection in your Nextmv Application. You can do +Start by creating a secrets collection in your Nextmv application. You can do this with the `new_secrets_collection` method. ```python diff --git a/docs/nextmv/cloud/versions.md b/docs/nextmv/cloud/versions.md index b5a197d..c741594 100644 --- a/docs/nextmv/cloud/versions.md +++ b/docs/nextmv/cloud/versions.md @@ -1,4 +1,4 @@ -# Manage Versions for an Application +# Manage Versions for an application !!! tip "Reference" @@ -10,7 +10,7 @@ developing new features. ## Understanding Versions -A Nextmv Cloud Application Version refers to a specific executable created from +A Nextmv Cloud application Version refers to a specific executable created from a pushed application. The underlying executable is immutable and can be configured to run in an [instance][instance]. diff --git a/docs/nextmv/local/get-started-existing-model.md b/docs/nextmv/local/get-started-existing-model.md index 465e714..49df488 100644 --- a/docs/nextmv/local/get-started-existing-model.md +++ b/docs/nextmv/local/get-started-existing-model.md @@ -222,11 +222,11 @@ Total load of all routes: 60 ## 4. Nextmv-ify the decision model -We are going to turn the executable decision model into a Nextmv Application. +We are going to turn the executable decision model into a Nextmv application. !!! abstract "Application" - So, what is a Nextmv Application? A Nextmv Application is an entity that + So, what is a Nextmv application? A Nextmv application is an entity that contains a decision model as executable code. An Application can make a run by taking an input, executing the decision model, and producing an output. An Application is defined by its code, and a configuration file @@ -235,7 +235,7 @@ We are going to turn the executable decision model into a Nextmv Application. Think of the app as a shell that contains your decision model code, and provides the necessary structure to run it. -A run on a Nextmv Application follows this convention: +A run on a Nextmv application follows this convention: ![App diagram][app-diagram] @@ -612,7 +612,7 @@ structure, for the example provided: └── requirements.txt ``` -You are ready to run your existing Nextmv Application locally using the +You are ready to run your existing Nextmv application locally using the `nextmv.local` package 🥳. ## 5. Start a run diff --git a/docs/nextmv/local/get-started-new-model.md b/docs/nextmv/local/get-started-new-model.md index 548b333..df89cd5 100644 --- a/docs/nextmv/local/get-started-new-model.md +++ b/docs/nextmv/local/get-started-new-model.md @@ -54,7 +54,7 @@ app- !!! abstract "Application" - So, what is a Nextmv Application? A Nextmv Application is an entity that + So, what is a Nextmv application? A Nextmv application is an entity that contains a decision model as executable code. An Application can make a run by taking an input, executing the decision model, and producing an output. An Application is defined by its code, and a configuration file @@ -509,7 +509,7 @@ more. You have successfully: -* Created a new Nextmv Application. +* Created a new Nextmv application. * Ran it locally. * Obtained run results. * Visualized assets. diff --git a/docs/nextmv/local/index.md b/docs/nextmv/local/index.md index 8740d55..a34c22d 100644 --- a/docs/nextmv/local/index.md +++ b/docs/nextmv/local/index.md @@ -1,6 +1,6 @@ # Local overview -The `nextmv.local` package provides functionality to run Nextmv Applications +The `nextmv.local` package provides functionality to run Nextmv applications locally on your machine. !!! success "Free & open-source" @@ -24,7 +24,7 @@ features of the local experience: | Feature | Description | |---------|-------------| -| [Get started - new model][get-started-new-model] | Create a new Nextmv Application and run it locally | +| [Get started - new model][get-started-new-model] | Create a new Nextmv application and run it locally | | [Get started - existing model][get-started-existing-model] | Run your existing Python decision model locally | | [Run an app][runs] | Create and manage runs for your local Application | | [Sync to Cloud][sync] | Push your local Application to Nextmv Cloud | diff --git a/docs/nextmv/local/sync.md b/docs/nextmv/local/sync.md index 9ceb09e..7361b21 100644 --- a/docs/nextmv/local/sync.md +++ b/docs/nextmv/local/sync.md @@ -10,7 +10,7 @@ Application][cloud-application]. All your local runs are structured under the `.nextmv` directory of your local app. The sync process will track all your local runs and push them to your -Cloud Application. +Cloud application. To unleash the full potential of Nextmv, let's create a [`cloud.Application`][cloud-application], and sync our @@ -23,7 +23,7 @@ you can take full advantage of the Nextmv Platform with features such as: --- -Let's start by creating a [Cloud Application][cloud-application]. +Let's start by creating a [Cloud application][cloud-application]. ```python import os @@ -61,7 +61,7 @@ You should see an output similar to the following: * You can specify the `run_ids` parameter to sync specific runs, as opposed to all the runs in your local app. * You can specify the `instance_id` parameter to sync the runs to a specific - [instance][instance] of your Cloud Application. + [instance][instance] of your Cloud application. What happens when you run the command again? diff --git a/mkdocs.yml b/mkdocs.yml index 584c701..d54910f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,6 +53,7 @@ nav: - Large payloads: nextmv/cloud/large-payloads.md - Queuing & prioritization: nextmv/cloud/queuing.md - Execution classes: nextmv/cloud/execution-classes.md + - Integrations: nextmv/cloud/integrations.md - Scenario tests: nextmv/cloud/scenario-tests.md - Batch experiments: nextmv/cloud/batch-experiments.md - Acceptance tests: nextmv/cloud/acceptance-tests.md @@ -66,6 +67,7 @@ nav: - ensemble.py: nextmv/cloud/reference/ensemble.md - input_set.py: nextmv/cloud/reference/input_set.md - instance.py: nextmv/cloud/reference/instance.md + - integration.py: nextmv/cloud/reference/integration.md - scenario.py: nextmv/cloud/reference/scenario.md - secrets.py: nextmv/cloud/reference/secrets.md - url.py: nextmv/cloud/reference/url.md diff --git a/nextmv/nextmv/cloud/__init__.py b/nextmv/nextmv/cloud/__init__.py index 6a18f38..de15d82 100644 --- a/nextmv/nextmv/cloud/__init__.py +++ b/nextmv/nextmv/cloud/__init__.py @@ -74,6 +74,10 @@ from .input_set import ManagedInput as ManagedInput from .instance import Instance as Instance from .instance import InstanceConfiguration as InstanceConfiguration +from .integration import Integration as Integration +from .integration import IntegrationProvider as IntegrationProvider +from .integration import IntegrationType as IntegrationType +from .integration import list_integrations as list_integrations from .scenario import Scenario as Scenario from .scenario import ScenarioConfiguration as ScenarioConfiguration from .scenario import ScenarioInput as ScenarioInput diff --git a/nextmv/nextmv/cloud/application.py b/nextmv/nextmv/cloud/application.py index 28dab5e..ba5f350 100644 --- a/nextmv/nextmv/cloud/application.py +++ b/nextmv/nextmv/cloud/application.py @@ -3215,7 +3215,7 @@ def update_instance( name: str | None = None, version_id: str | None = None, description: str | None = None, - configuration: InstanceConfiguration | None = None, + configuration: InstanceConfiguration | dict[str, Any] | None = None, ) -> Instance: """ Update an instance. @@ -3230,7 +3230,7 @@ def update_instance( Optional ID of the version to associate the instance with. description : Optional[str], default=None Optional description of the instance. - configuration : Optional[InstanceConfiguration], default=None + configuration : Optional[InstanceConfiguration | dict[str, Any]], default=None Optional configuration to use for the instance. Returns @@ -3247,13 +3247,7 @@ def update_instance( # Get the instance as it currently exsits. instance = self.instance(id) instance_dict = instance.to_dict() - - payload = { - "name": instance_dict["name"], - "version_id": instance_dict["version_id"], - "description": instance_dict["description"], - "configuration": instance_dict["configuration"], - } + payload = instance_dict if name is not None: payload["name"] = name @@ -3262,7 +3256,14 @@ def update_instance( if description is not None: payload["description"] = description if configuration is not None: - payload["configuration"] = configuration.to_dict() + if isinstance(configuration, dict): + config_dict = configuration + elif isinstance(configuration, InstanceConfiguration): + config_dict = configuration.to_dict() + else: + raise TypeError("configuration must be either a dict or InstanceConfiguration object") + + payload["configuration"] = config_dict response = self.client.request( method="PUT", @@ -3303,11 +3304,7 @@ def update_managed_input( managed_input = self.managed_input(managed_input_id) managed_input_dict = managed_input.to_dict() - - payload = { - "name": managed_input_dict["name"], - "description": managed_input_dict["description"], - } + payload = managed_input_dict if name is not None: payload["name"] = name @@ -3376,7 +3373,7 @@ def update_secrets_collection( secrets_collection_id: str, name: str | None = None, description: str | None = None, - secrets: list[Secret] | None = None, + secrets: list[Secret | dict[str, Any]] | None = None, ) -> SecretsCollectionSummary: """ Update a secrets collection. @@ -3393,7 +3390,7 @@ def update_secrets_collection( Optional new name for the secrets collection. description : Optional[str], default=None Optional new description for the secrets collection. - secrets : Optional[list[Secret]], default=None + secrets : Optional[list[Secret | dict[str, Any]]], default=None Optional list of secrets to update. Each secret should be an instance of the Secret class containing a key and value. @@ -3429,19 +3426,23 @@ def update_secrets_collection( collection = self.secrets_collection(secrets_collection_id) collection_dict = collection.to_dict() - - payload = { - "name": collection_dict["name"], - "description": collection_dict["description"], - "secrets": collection_dict["secrets"], - } + payload = collection_dict if name is not None: payload["name"] = name if description is not None: payload["description"] = description if secrets is not None and len(secrets) > 0: - payload["secrets"] = [secret.to_dict() for secret in secrets] + secrets_dicts = [] + for ix, secret in enumerate(secrets): + if isinstance(secret, dict): + secrets_dicts.append(secret) + elif isinstance(secret, Secret): + secrets_dicts.append(secret.to_dict()) + else: + raise ValueError(f"secret at index {ix} must be either a Secret or dict object") + + payload["secrets"] = secrets_dicts response = self.client.request( method="PUT", diff --git a/nextmv/nextmv/cloud/integration.py b/nextmv/nextmv/cloud/integration.py new file mode 100644 index 0000000..975ee0a --- /dev/null +++ b/nextmv/nextmv/cloud/integration.py @@ -0,0 +1,533 @@ +""" +Integration module for interacting with Nextmv Cloud integrations. + +This module provides functionality to interact with integrations in Nextmv +Cloud, including integration management. + +Classes +------- +IntegrationType + Enum representing the type of an integration. +IntegrationProvider + Enum representing the provider of an integration. +Integration + Class representing an integration in Nextmv Cloud. + +Functions +--------- +list_integrations + Function to list all integrations in Nextmv Cloud. +""" + +from datetime import datetime +from enum import Enum +from typing import Any + +from pydantic import AliasChoices, Field + +from nextmv.base_model import BaseModel +from nextmv.cloud.client import Client +from nextmv.manifest import ManifestType +from nextmv.safe import safe_id + + +class IntegrationType(str, Enum): + """ + The type of an integration. + + You can import the `IntegrationType` class directly from `cloud`: + + ```python + from nextmv.cloud import IntegrationType + ``` + + Attributes + ---------- + RUNTIME : str + Indicates a runtime integration. + DATA : str + Indicates a data integration. + """ + + RUNTIME = "runtime" + """Indicates a runtime integration.""" + DATA = "data" + """Indicates a data integration.""" + + +class IntegrationProvider(str, Enum): + """ + The provider of an integration. + + You can import the `IntegrationProvider` class directly from `cloud`: + + ```python + from nextmv.cloud import IntegrationProvider + ``` + + Attributes + ---------- + DBX : str + Indicates a Databricks integration. + UNKNOWN : str + Indicates an unknown integration provider. + """ + + DBX = "dbx" + """Indicates a Databricks integration.""" + UNKNOWN = "unknown" + """Indicates an unknown integration provider.""" + + +class Integration(BaseModel): + """ + Represents an integration in Nextmv Cloud. An integration allows Nextmv + Cloud to communicate with external systems or services. + + You can import the `Integration` class directly from `cloud`: + + ```python + from nextmv.cloud import Integration + ``` + + You can use the `Integration.get` class method to retrieve an existing + integration from Nextmv Cloud, to ensure that all fields are properly + populated. + + Parameters + ---------- + integration_id : str + The unique identifier of the integration. + client : Client + Client to use for interacting with the Nextmv Cloud API. + name : str, optional + The name of the integration. + description : str, optional + An optional description of the integration. + is_global : bool, optional + Indicates whether the integration is global (available to all + applications in the account). + application_ids : list[str], optional + List of application IDs that have access to this integration. + integration_type : IntegrationType, optional + The type of the integration (runtime or data). + exec_types : list[ManifestType], optional + List of execution types supported by the integration. + provider : IntegrationProvider, optional + The provider of the integration. + provider_config : dict[str, Any], optional + Configuration specific to the integration provider. + created_at : datetime, optional + The timestamp when the integration was created. + updated_at : datetime, optional + The timestamp when the integration was last updated. + """ + + integration_id: str = Field( + serialization_alias="id", + validation_alias=AliasChoices("id", "integration_id"), + ) + """The unique identifier of the integration.""" + client: Client = Field(exclude=True) + """Client to use for interacting with the Nextmv Cloud API.""" + + name: str | None = None + """The name of the integration.""" + description: str | None = None + """An optional description of the integration.""" + is_global: bool = Field( + serialization_alias="global", + validation_alias=AliasChoices("global", "is_global"), + default=False, + ) + """ + Indicates whether the integration is global (available to all + applications in the account). + """ + application_ids: list[str] | None = None + """ + List of application IDs that have access to this integration. + """ + integration_type: IntegrationType | None = Field( + serialization_alias="type", + validation_alias=AliasChoices("type", "integration_type"), + default=None, + ) + """The type of the integration (runtime or data).""" + exec_types: list[ManifestType] | None = None + """List of execution types supported by the integration.""" + provider: IntegrationProvider | None = None + """The provider of the integration.""" + provider_config: dict[str, Any] | None = None + """Configuration specific to the integration provider.""" + created_at: datetime | None = None + """The timestamp when the integration was created.""" + updated_at: datetime | None = None + """The timestamp when the integration was last updated.""" + endpoint: str = Field( + exclude=True, + default="v1/integrations/{id}", + ) + """Base endpoint for the integration.""" + + def model_post_init(self, __context) -> None: + """ + Validations done after model initialization. + """ + + self.endpoint = self.endpoint.format(id=self.integration_id) + + @classmethod + def get(cls, client: Client, integration_id: str) -> "Integration": + """ + Retrieve an existing integration from Nextmv Cloud. + + This method should be used for validating that the integration exists, + and not rely simply on instantiating the `Integration` class. Using + this method ensures that all the fields of the `Integration` class are + properly populated. + + Parameters + ---------- + client : Client + Client to use for interacting with the Nextmv Cloud API. + integration_id : str + The unique identifier of the integration to retrieve. + + Returns + ------- + Integration + The retrieved integration instance. + + Raises + ------ + requests.HTTPError + If the response status code is not 2xx. + + Examples + -------- + >>> from nextmv.cloud import Client, Integration + >>> client = Client(api_key="your_api_key") + >>> integration = Integration.get(client=client, integration_id="your_integration_id") + >>> print(integration.to_dict()) + """ + + response = client.request( + method="GET", + endpoint=f"v1/integrations/{integration_id}", + ) + response_dict = response.json() + response_dict["client"] = client + + return cls.from_dict(response_dict) + + @classmethod + def new( # noqa: C901 + cls, + client: Client, + name: str, + integration_type: IntegrationType | str, + exec_types: list[ManifestType | str], + provider: IntegrationProvider | str, + provider_config: dict[str, Any], + integration_id: str | None = None, + description: str | None = None, + is_global: bool = False, + application_ids: list[str] | None = None, + exist_ok: bool = False, + ) -> "Integration": + """ + Create a new integration directly in Nextmv Cloud. + + Parameters + ---------- + client : Client + Client to use for interacting with the Nextmv Cloud API. + name : str + The name of the integration. + integration_type : IntegrationType | str + The type of the integration. Please refer to the `IntegrationType` + enum for possible values. + exec_types : list[ManifestType | str] + List of execution types supported by the integration. Please refer + to the `ManifestType` enum for possible values. + provider : IntegrationProvider | str + The provider of the integration. Please refer to the + `IntegrationProvider` enum for possible values. + provider_config : dict[str, Any] + Configuration specific to the integration provider. + integration_id : str, optional + The unique identifier of the integration. If not provided, + it will be generated automatically. + description : str, optional + An optional description of the integration. + is_global : bool, optional, default=False + Indicates whether the integration is global (available to all + applications in the account). Default is False. + application_ids : list[str], optional + List of application IDs that have access to this integration. + exist_ok : bool, default=False + If True and an integration with the same ID already exists, + return the existing integration instead of creating a new one. + + Returns + ------- + Integration + The created integration instance. + + Raises + ------ + requests.HTTPError + If the response status code is not 2xx. + ValueError + If both `is_global` is True and `application_ids` is provided. + + Examples + -------- + >>> from nextmv.cloud import Client, Integration, IntegrationType, IntegrationProvider, ManifestType + >>> client = Client(api_key="your_api_key") + >>> integration = Integration.new( + ... client=client, + ... name="my_integration", + ... integration_type=IntegrationType.RUNTIME, + ... exec_types=[ManifestType.PYTHON], + ... provider=IntegrationProvider.DBX, + ... provider_config={"config_key": "config_value"}, + ... ) + >>> print(integration.to_dict()) + """ + + if is_global and application_ids is not None: + raise ValueError("An integration cannot be global and have specific application IDs.") + elif not is_global and application_ids is None: + raise ValueError("A non-global integration must have specific application IDs.") + + if integration_id is None: + integration_id = safe_id("integration") + + if exist_ok: + try: + integration = cls.get(client=client, integration_id=integration_id) + return integration + except Exception: + pass + + if not isinstance(integration_type, IntegrationType): + integration_type = IntegrationType(integration_type) + + if not all(isinstance(exec_type, ManifestType) for exec_type in exec_types): + exec_types = [ManifestType(exec_type) for exec_type in exec_types] + + if not isinstance(provider, IntegrationProvider): + provider = IntegrationProvider(provider) + + payload = { + "id": integration_id, + "name": name, + "global": is_global, + "type": integration_type.value, + "exec_types": [exec_type.value for exec_type in exec_types], + "provider": provider.value, + "provider_config": provider_config, + } + + if description is not None: + payload["description"] = description + + if application_ids is not None: + payload["application_ids"] = application_ids + + response = client.request( + method="POST", + endpoint="v1/integrations", + payload=payload, + ) + response_dict = response.json() + response_dict["client"] = client + integration = cls.from_dict(response_dict) + + return integration + + def delete(self) -> None: + """ + Deletes the integration from Nextmv Cloud. + + Raises + ------ + requests.HTTPError + If the response status code is not 2xx. + + Examples + -------- + >>> from nextmv.cloud import Client, Integration + >>> client = Client(api_key="your_api_key") + >>> integration = Integration.get(client=client, integration_id="your_integration_id") + >>> integration.delete() + """ + + _ = self.client.request( + method="DELETE", + endpoint=self.endpoint, + ) + + def update( # noqa: C901 + self, + name: str | None = None, + integration_type: IntegrationType | str | None = None, + exec_types: list[ManifestType | str] | None = None, + provider: IntegrationProvider | str | None = None, + provider_config: dict[str, Any] | None = None, + description: str | None = None, + is_global: bool | None = None, + application_ids: list[str] | None = None, + ) -> "Integration": + """ + Updates the integration in Nextmv Cloud. + + Parameters + ---------- + name : str, optional + The new name of the integration. + integration_type : IntegrationType | str, optional + The new type of the integration. Please refer to the `IntegrationType` + enum for possible values. + exec_types : list[ManifestType | str], optional + New list of execution types supported by the integration. Please refer + to the `ManifestType` enum for possible values. + provider : IntegrationProvider | str, optional + The new provider of the integration. Please refer to the + `IntegrationProvider` enum for possible values. + provider_config : dict[str, Any], optional + New configuration specific to the integration provider. + description : str, optional + The new description of the integration. + is_global : bool, optional + Indicates whether the integration is global (available to all + applications in the account). If not provided, the current value + is preserved. + application_ids : list[str], optional + New list of application IDs that have access to this integration. + + Returns + ------- + Integration + The updated integration instance. + + Raises + ------ + requests.HTTPError + If the response status code is not 2xx. + + Examples + -------- + >>> from nextmv.cloud import Client, Integration + >>> client = Client(api_key="your_api_key") + >>> integration = Integration.get(client=client, integration_id="your_integration_id") + >>> updated_integration = integration.update(name="new_name") + >>> print(updated_integration.to_dict()) + """ + + integration = self.get(client=self.client, integration_id=self.integration_id) + integration_dict = integration.to_dict() + payload = integration_dict + + if name is not None: + payload["name"] = name + + if integration_type is not None: + if not isinstance(integration_type, IntegrationType): + integration_type = IntegrationType(integration_type) + payload["type"] = integration_type.value + + if exec_types is not None: + if not all(isinstance(exec_type, ManifestType) for exec_type in exec_types): + exec_types = [ManifestType(exec_type) for exec_type in exec_types] + payload["exec_types"] = [exec_type.value for exec_type in exec_types] + + if provider is not None: + if not isinstance(provider, IntegrationProvider): + provider = IntegrationProvider(provider) + payload["provider"] = provider.value + + if provider_config is not None: + payload["provider_config"] = provider_config + + if description is not None: + payload["description"] = description + + if is_global is not None: + payload["global"] = is_global + + if application_ids is not None: + payload["application_ids"] = application_ids + + # Final validation: ensure invariants are met. + if payload["global"] is True and payload.get("application_ids"): + raise ValueError( + "An integration cannot be global and have application_ids. " + "To make an integration global, call update(is_global=True, application_ids=[])." + ) + if payload["global"] is False and not payload.get("application_ids"): + raise ValueError( + "A non-global integration must have specific application IDs. " + "Provide application_ids with at least one ID, or set is_global=True." + ) + + response = self.client.request( + method="PUT", + endpoint=self.endpoint, + payload=payload, + ) + response_dict = response.json() + response_dict["client"] = self.client + integration = self.from_dict(response_dict) + + return integration + + +def list_integrations(client: Client) -> list[Integration]: + """ + List all integrations in Nextmv Cloud for the given client. + + You can import the `list_integrations` method directly from `cloud`: + + ```python + from nextmv.cloud import list_integrations + ``` + + Parameters + ---------- + client : Client + Client to use for interacting with the Nextmv Cloud API. + + Returns + ------- + list[Integration] + List of integrations. + + Raises + ------ + requests.HTTPError + If the response status code is not 2xx. + + Examples + -------- + >>> from nextmv.cloud import Client, list_integrations + >>> client = Client(api_key="your_api_key") + >>> integrations = list_integrations(client=client) + >>> for integration in integrations: + ... print(integration.to_dict()) + """ + + response = client.request( + method="GET", + endpoint="v1/integrations", + ) + response_dict = response.json() + integrations = [] + for integration_data in response_dict.get("items", []): + integration_data["client"] = client + integration = Integration.from_dict(integration_data) + integrations.append(integration) + + return integrations diff --git a/nextmv/nextmv/default_app/README.md b/nextmv/nextmv/default_app/README.md index 8b37bf0..642844a 100644 --- a/nextmv/nextmv/default_app/README.md +++ b/nextmv/nextmv/default_app/README.md @@ -1,4 +1,4 @@ -# Nextmv Application +# Nextmv application This is the basic structure of a Nextmv application. diff --git a/nextmv/nextmv/input.py b/nextmv/nextmv/input.py index 73e122d..741a24e 100644 --- a/nextmv/nextmv/input.py +++ b/nextmv/nextmv/input.py @@ -998,7 +998,7 @@ def load_local( deprecated( name="load_local", - reason="`load_local` is deprecated, use `load` instead.", + reason="`load_local` is deprecated, use `load` instead", ) loader = LocalInputLoader() diff --git a/nextmv/nextmv/local/application.py b/nextmv/nextmv/local/application.py index 7c21eb3..8754d89 100644 --- a/nextmv/nextmv/local/application.py +++ b/nextmv/nextmv/local/application.py @@ -7,7 +7,7 @@ Classes ------- Application - Class for interacting with local Nextmv Applications. + Class for interacting with local Nextmv applications. """ import json diff --git a/nextmv/nextmv/output.py b/nextmv/nextmv/output.py index ae4af62..862b29b 100644 --- a/nextmv/nextmv/output.py +++ b/nextmv/nextmv/output.py @@ -1564,7 +1564,7 @@ def write_local( deprecated( name="write_local", - reason="`write_local` is deprecated, use `write` instead.", + reason="`write_local` is deprecated, use `write` instead", ) writer = LocalOutputWriter()