diff --git a/CHANGELOG.md b/CHANGELOG.md index 92817ef..d10bdfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [8.44.0](https://github.com/Arize-ai/arize/compare/arize-python-sdk/v8.43.1...arize-python-sdk/v8.44.0) (2026-08-04) + + +### 🎁 New Features + +* **experiments:** Enable standalone experiment creation ([#81022](https://github.com/Arize-ai/arize/issues/81022)) ([a1a11b2](https://github.com/Arize-ai/arize/commit/a1a11b212e6cb49d9dad1522afa1e08a549c6c64)) +* **integrations:** add integrations subclient (llm+agent CRUD) + agent_call task support ([#79759](https://github.com/Arize-ai/arize/issues/79759)) ([9ab254a](https://github.com/Arize-ai/arize/commit/9ab254a9c086b57d7f746fb94872d7245831a3c5)) + ## [8.43.1](https://github.com/Arize-ai/arize/compare/arize-python-sdk/v8.43.0...arize-python-sdk/v8.43.1) (2026-07-27) diff --git a/README.md b/README.md index fa56b3f..416ffc7 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,13 @@ - [Get an AI Integration](#get-an-ai-integration) - [Update an AI Integration](#update-an-ai-integration) - [Delete an AI Integration](#delete-an-ai-integration) + - [Operations on Integrations](#operations-on-integrations) + - [List Integrations](#list-integrations) + - [Get an Integration](#get-an-integration) + - [Create an LLM Integration](#create-an-llm-integration) + - [Create an Agent Integration](#create-an-agent-integration) + - [Update an Integration](#update-an-integration) + - [Delete an Integration](#delete-an-integration) - [SDK Configuration](#sdk-configuration) - [Logging](#logging) - [In Code](#in-code) @@ -617,7 +624,7 @@ not_deleted = resp.not_deleted_example_ids ### List Experiments -You can list all experiments that the user has access to using `client.experiments.list()`. You can use the `limit` parameter to specify the maximum number of experiments desired in the response and you can specify the `dataset` to target the list operation to a particular dataset. +You can list all experiments that the user has access to using `client.experiments.list()`. You can use the `limit` parameter to specify the maximum number of experiments desired in the response, and narrow the results by scope: pass `dataset` for only the experiments run on that dataset, or `space` for every experiment in that space — including those with no dataset. The two are mutually exclusive. ```python resp = client.experiments.list( @@ -687,22 +694,39 @@ It is possible that you have run the experiment yourself without the above funct > NOTE: If you don't have experiment data and want to run an experiment, see the `client.experiments.run()` section above. -In addition, you must specify which columns are the `example_id` and the `result` using `ExperimentTaskFieldNames`. If you have evaluation data, indicate the evaluation columns using `EvaluationResultFieldNames`. +An experiment belongs to a space and may optionally be associated with a dataset. Pass exactly one of: -If the number of runs is too large, the client SDK will try to send the data via Arrow Flight via gRPC for better performance. If you want to force the data transfer to HTTP you can use the `force_http` flag. The response is an `Experiment` object. +- `dataset` — associates the experiment with a dataset, so its runs can reference that dataset's examples. +- `space` — creates the experiment directly in a space, with no dataset. + +In addition, you must specify which column holds the `output` using `ExperimentTaskFieldNames`. Also set `example_id` there when you pass `dataset`, so each run can be matched to the example it ran against; it isn't used for an experiment with no dataset. If you have evaluation data, indicate the evaluation columns using `EvaluationResultFieldNames`. + +If the number of runs is too large, the client SDK will try to send the data via Arrow Flight via gRPC for better performance. If you want to force the data transfer to HTTP you can use the `force_http` flag. Experiments with no dataset always upload via HTTP, since the gRPC + Flight path only supports dataset-associated experiments. The response is an `Experiment` object. ```python from arize.experiments.types import ExperimentTaskFieldNames, EvaluationResultFieldNames +# Associated with a dataset: runs reference the dataset's examples. created_experiment = client.experiments.create( - name="", # Name must be unique within a dataset + name="", # Name must be unique within the dataset dataset="", - space=..., # Optional, space ID or name + space=..., # Optional, space ID or name, used to resolve `dataset` by name experiment_runs=..., # List of dictionaries or pandas dataframe - task_fields=ExperimentTaskFieldNames(...), + task_fields=ExperimentTaskFieldNames( + output="", + example_id="", + ), evaluator_columns=... # Optional # force_http=... # Optionally pass force_http to create experiments via HTTP instead of gRPC, defaults to False ) + +# No dataset: created directly in a space, and runs need no example ID. +standalone_experiment = client.experiments.create( + name="", # Name must be unique within the space + space="", + experiment_runs=..., + task_fields=ExperimentTaskFieldNames(output=""), +) ``` ### Get an Experiment @@ -712,8 +736,10 @@ To get an experiment by its ID or name use `client.experiments.get()`. The retur ```python experiment = client.experiments.get( experiment=... # The experiment ID or name - dataset=... # Optional, dataset ID or name (required when looking up by experiment name) - space=... # Optional, space ID or name + dataset=... # Optional, dataset ID or name, to look up by name within a dataset + space=... # Optional, space ID or name; looks up by name within the space when + # `dataset` is omitted, which is the only option for an experiment + # with no dataset ) ``` @@ -758,7 +784,7 @@ resp_df = resp.to_df() ### Append Experiment Runs -Append between 1 and 1000 new runs to an existing experiment using `client.experiments.append_runs()`. Each run must include `example_id` (the ID of an example from the experiment's dataset) and `output`. The response includes the updated experiment and the generated run IDs in input order (`run_ids`). +Append between 1 and 1000 new runs to an existing experiment using `client.experiments.append_runs()`. Each run must include `output`; `example_id` (the ID of an example from the experiment's dataset) is required only when the target experiment is associated with a dataset. The response includes the updated experiment and the generated run IDs in input order (`run_ids`). ```python result = client.experiments.append_runs( @@ -1758,6 +1784,126 @@ client.ai_integrations.delete( ) ``` +## Operations on Integrations + +Use `client.integrations` to manage **agent** and **LLM** integrations. Integrations are polymorphic: LLM integrations configure a model provider, while agent integrations connect a customer-hosted agent exposed at an HTTPS endpoint. Names are only unique per `(account, type)`, so an `integration_type` is required to resolve a name (an ID needs no type); `space` is only an optional visibility filter. + +> **Note:** This is distinct from `client.ai_integrations` (the legacy AI integrations used by the Playground and online evaluations). + +> **Note:** Integrations are an **alpha** endpoint. Enable it via the pre-release opt-in; the surface may change without notice. + +### List Integrations + +When `integration_type` is omitted, integrations of every type are returned in one merged list; each item carries its `type`. + +```python +from arize.integrations.types import IntegrationType + +resp = client.integrations.list( + integration_type=..., # Optional, IntegrationType.LLM or IntegrationType.AGENT + name=..., # Optional, case-insensitive substring filter + space=..., # Optional, space ID or name (visibility filter) + limit=..., # Optional, defaults to 50 + cursor=..., # Optional, pagination cursor from a previous response +) +integration_list = resp.integrations +``` + +### Get an Integration + +```python +from arize.integrations.types import IntegrationType + +integration = client.integrations.get( + integration="", + integration_type=IntegrationType.LLM, # Required to resolve a name; not needed for an ID + space=..., # Optional, visibility filter +) +``` + +### Create an LLM Integration + +LLM integrations configure access to a model provider. Construct the generated config that matches the provider you want — all 7 are supported: `CreateOpenAiConfig`, `CreateAnthropicConfig`, `CreateGeminiConfig`, `CreateAwsBedrockConfig`, `CreateCustomConfig`, `CreateVertexAiConfig`, and `CreateNvidiaNimConfig`. + +```python +from arize.integrations.types import CreateOpenAiConfig + +integration = client.integrations.create_llm( + name="my-openai-integration", # Must be unique within the account per type + config=CreateOpenAiConfig( + provider="OPEN_AI", + api_key="", + ), + scopings=..., # Optional, visibility scoping rules (defaults to account-wide) +) +``` + +### Create an Agent Integration + +Agent integrations connect a customer-hosted agent exposed at an HTTPS endpoint. + +```python +from arize.integrations.types import CreateAgentRequestPresetInput + +integration = client.integrations.create_agent( + name="my-agent", # Must be unique within the account per type + endpoint="https://my-agent.example.com/replay", # Validated for SSRF server-side + input_schema={ + "type": "object", + "properties": {"input": {"type": "string"}}, + "required": ["input"], + }, + description=..., # Optional + headers=..., # Optional, custom headers (encrypted at rest, never returned) + request_presets=[ # Optional, list of CreateAgentRequestPresetInput + CreateAgentRequestPresetInput(name="default", config={"input": "hello"}), + ], + scopings=..., # Optional, visibility scoping rules +) +``` + +### Update an Integration + +Only the fields you pass are sent; omitted fields are left unchanged. Use the method matching the integration's type (the provider/type is immutable). Nullable fields accept an explicit `None` to clear them. + +```python +# Update an LLM integration (e.g. rotate the API key) +integration = client.integrations.update_llm( + integration="", + space=..., # Optional, visibility filter + name=..., # Optional + api_key=..., # Optional, pass None to clear + function_calling_enabled=..., # Optional + # Provider-conditional fields are also accepted: base_url, headers, + # model_names, is_default_models_enabled, auth (AWS Bedrock), + # project_id, location, project_access_label (Vertex AI). +) + +# Update an agent integration +integration = client.integrations.update_agent( + integration="", + space=..., # Optional + name=..., # Optional + description=..., # Optional, pass None to clear + endpoint=..., # Optional + input_schema=..., # Optional + headers=..., # Optional, pass None to clear + request_presets=..., # Optional, replaces existing presets (matched by name) +) +``` + +### Delete an Integration + +```python +from arize.integrations.types import IntegrationType + +client.integrations.delete( + integration="", + integration_type=IntegrationType.AGENT, # Required to resolve a name; not needed for an ID + space=..., # Optional +) +``` + # SDK Configuration ## Logging diff --git a/docs/source/_static/switcher.json b/docs/source/_static/switcher.json index 5f0c2b7..3804a3e 100644 --- a/docs/source/_static/switcher.json +++ b/docs/source/_static/switcher.json @@ -4,6 +4,10 @@ "url": "https://arize-client-python.readthedocs.io/en/latest/", "preferred": true }, + { + "version": "v8.44.0", + "url": "https://arize-client-python.readthedocs.io/en/v8.44.0/" + }, { "version": "v8.43.0", "url": "https://arize-client-python.readthedocs.io/en/v8.43.0/" @@ -40,10 +44,6 @@ "version": "v8.35.0", "url": "https://arize-client-python.readthedocs.io/en/v8.35.0/" }, - { - "version": "v8.34.0", - "url": "https://arize-client-python.readthedocs.io/en/v8.34.0/" - }, { "version": "v7.52.0", "url": "https://arize-client-python.readthedocs.io/en/v7.52.0/" diff --git a/docs/source/index.md b/docs/source/index.md index 3d2e411..23b711d 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -21,6 +21,7 @@ api_keys datasets evaluators experiments +integrations ml organizations projects diff --git a/docs/source/integrations.md b/docs/source/integrations.md new file mode 100644 index 0000000..33134f8 --- /dev/null +++ b/docs/source/integrations.md @@ -0,0 +1,16 @@ +# Integrations + +```{eval-rst} +.. currentmodule:: arize.integrations.client +.. autoclass:: IntegrationsClient + :members: + :member-order: bysource +``` + +## Response Types + +```{eval-rst} +.. automodule:: arize.integrations.types + :members: + :member-order: bysource +``` diff --git a/src/arize/_generated/api_client/api/annotation_configs_api.py b/src/arize/_generated/api_client/api/annotation_configs_api.py index b3c2436..8506e55 100644 --- a/src/arize/_generated/api_client/api/annotation_configs_api.py +++ b/src/arize/_generated/api_client/api/annotation_configs_api.py @@ -357,7 +357,7 @@ def delete_annotation_config( ) -> None: """Delete an annotation config - Delete an annotation config by its ID. This operation is irreversible. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Delete an annotation config by its ID. The annotation config must not be associated with an active annotation queue; remove it from those queues before deleting it. This operation is irreversible. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param annotation_config_id: The unique annotation config identifier (base64) (required) :type annotation_config_id: str @@ -397,6 +397,7 @@ def delete_annotation_config( '401': "Problem", '403': "Problem", '404': "Problem", + '409': "Problem", '429': "Problem", } response_data = self.api_client.call_api( @@ -429,7 +430,7 @@ def delete_annotation_config_with_http_info( ) -> ApiResponse[None]: """Delete an annotation config - Delete an annotation config by its ID. This operation is irreversible. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Delete an annotation config by its ID. The annotation config must not be associated with an active annotation queue; remove it from those queues before deleting it. This operation is irreversible. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param annotation_config_id: The unique annotation config identifier (base64) (required) :type annotation_config_id: str @@ -469,6 +470,7 @@ def delete_annotation_config_with_http_info( '401': "Problem", '403': "Problem", '404': "Problem", + '409': "Problem", '429': "Problem", } response_data = self.api_client.call_api( @@ -501,7 +503,7 @@ def delete_annotation_config_without_preload_content( ) -> RESTResponseType: """Delete an annotation config - Delete an annotation config by its ID. This operation is irreversible. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Delete an annotation config by its ID. The annotation config must not be associated with an active annotation queue; remove it from those queues before deleting it. This operation is irreversible. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param annotation_config_id: The unique annotation config identifier (base64) (required) :type annotation_config_id: str @@ -541,6 +543,7 @@ def delete_annotation_config_without_preload_content( '401': "Problem", '403': "Problem", '404': "Problem", + '409': "Problem", '429': "Problem", } response_data = self.api_client.call_api( diff --git a/src/arize/_generated/api_client/api/annotation_queues_api.py b/src/arize/_generated/api_client/api/annotation_queues_api.py index 4ccf4ec..e93483a 100644 --- a/src/arize/_generated/api_client/api/annotation_queues_api.py +++ b/src/arize/_generated/api_client/api/annotation_queues_api.py @@ -1012,7 +1012,7 @@ def create_annotation_queue_record( ) -> CreateAnnotationQueueRecordResponse: """Create annotation queue records - Add new records from spans, traces, or dataset examples to an existing annotation queue. **Payload Requirements** - At least one record source is required. - At most 2 record sources are allowed per request - For span record source: `start_time` must be before `end_time`, and the range must not exceed 7 days. - For dataset record source: all `example_ids` must be non-empty strings. - For project record source: - span records: all `span_ids` must be non-empty strings. - trace records: all `trace_ids` must be non-empty strings. - At most 500 records total may be added in one request **Valid example (span record)** ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` **Valid example (trace record)** ```json { \"record_sources\": [ { \"record_type\": \"TRACE\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"trace_ids\": [\"8fe3373f-0da4-4a8e-b57f-5c8878cfb747\"] } ] } ``` **Invalid example** (span record with `start_time` after `end_time`) ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-20T00:00:00Z\", \"end_time\": \"2026-01-15T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` If no example_ids are provided for a dataset record source, all examples in the dataset will be added to the queue. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Add new records from spans, traces, or dataset examples to an existing annotation queue. **Payload Requirements** - At least one record source is required. - At most 2 record sources are allowed per request - For span record source: `start_time` must be before `end_time`, and the range must not exceed 7 days. - For dataset record source: all `example_ids` must be non-empty strings. - For project record source: - span records: all `span_ids` must be non-empty strings. - trace records: all `trace_ids` must be non-empty strings. - At most 500 records total may be added in one request **Valid example (span record)** ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` **Valid example (trace record)** ```json { \"record_sources\": [ { \"record_type\": \"TRACE\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"trace_ids\": [\"8fe3373f-0da4-4a8e-b57f-5c8878cfb747\"] } ] } ``` **Invalid example** (span record with `start_time` after `end_time`) ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-20T00:00:00Z\", \"end_time\": \"2026-01-15T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` If no example_ids are provided for a dataset record source, all examples in the dataset will be added to the queue only when the total records from all sources does not exceed 500. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param annotation_queue_id: The unique annotation queue identifier (base64) (required) :type annotation_queue_id: str @@ -1090,7 +1090,7 @@ def create_annotation_queue_record_with_http_info( ) -> ApiResponse[CreateAnnotationQueueRecordResponse]: """Create annotation queue records - Add new records from spans, traces, or dataset examples to an existing annotation queue. **Payload Requirements** - At least one record source is required. - At most 2 record sources are allowed per request - For span record source: `start_time` must be before `end_time`, and the range must not exceed 7 days. - For dataset record source: all `example_ids` must be non-empty strings. - For project record source: - span records: all `span_ids` must be non-empty strings. - trace records: all `trace_ids` must be non-empty strings. - At most 500 records total may be added in one request **Valid example (span record)** ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` **Valid example (trace record)** ```json { \"record_sources\": [ { \"record_type\": \"TRACE\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"trace_ids\": [\"8fe3373f-0da4-4a8e-b57f-5c8878cfb747\"] } ] } ``` **Invalid example** (span record with `start_time` after `end_time`) ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-20T00:00:00Z\", \"end_time\": \"2026-01-15T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` If no example_ids are provided for a dataset record source, all examples in the dataset will be added to the queue. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Add new records from spans, traces, or dataset examples to an existing annotation queue. **Payload Requirements** - At least one record source is required. - At most 2 record sources are allowed per request - For span record source: `start_time` must be before `end_time`, and the range must not exceed 7 days. - For dataset record source: all `example_ids` must be non-empty strings. - For project record source: - span records: all `span_ids` must be non-empty strings. - trace records: all `trace_ids` must be non-empty strings. - At most 500 records total may be added in one request **Valid example (span record)** ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` **Valid example (trace record)** ```json { \"record_sources\": [ { \"record_type\": \"TRACE\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"trace_ids\": [\"8fe3373f-0da4-4a8e-b57f-5c8878cfb747\"] } ] } ``` **Invalid example** (span record with `start_time` after `end_time`) ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-20T00:00:00Z\", \"end_time\": \"2026-01-15T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` If no example_ids are provided for a dataset record source, all examples in the dataset will be added to the queue only when the total records from all sources does not exceed 500. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param annotation_queue_id: The unique annotation queue identifier (base64) (required) :type annotation_queue_id: str @@ -1168,7 +1168,7 @@ def create_annotation_queue_record_without_preload_content( ) -> RESTResponseType: """Create annotation queue records - Add new records from spans, traces, or dataset examples to an existing annotation queue. **Payload Requirements** - At least one record source is required. - At most 2 record sources are allowed per request - For span record source: `start_time` must be before `end_time`, and the range must not exceed 7 days. - For dataset record source: all `example_ids` must be non-empty strings. - For project record source: - span records: all `span_ids` must be non-empty strings. - trace records: all `trace_ids` must be non-empty strings. - At most 500 records total may be added in one request **Valid example (span record)** ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` **Valid example (trace record)** ```json { \"record_sources\": [ { \"record_type\": \"TRACE\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"trace_ids\": [\"8fe3373f-0da4-4a8e-b57f-5c8878cfb747\"] } ] } ``` **Invalid example** (span record with `start_time` after `end_time`) ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-20T00:00:00Z\", \"end_time\": \"2026-01-15T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` If no example_ids are provided for a dataset record source, all examples in the dataset will be added to the queue. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Add new records from spans, traces, or dataset examples to an existing annotation queue. **Payload Requirements** - At least one record source is required. - At most 2 record sources are allowed per request - For span record source: `start_time` must be before `end_time`, and the range must not exceed 7 days. - For dataset record source: all `example_ids` must be non-empty strings. - For project record source: - span records: all `span_ids` must be non-empty strings. - trace records: all `trace_ids` must be non-empty strings. - At most 500 records total may be added in one request **Valid example (span record)** ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` **Valid example (trace record)** ```json { \"record_sources\": [ { \"record_type\": \"TRACE\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-15T00:00:00Z\", \"end_time\": \"2026-01-16T00:00:00Z\", \"trace_ids\": [\"8fe3373f-0da4-4a8e-b57f-5c8878cfb747\"] } ] } ``` **Invalid example** (span record with `start_time` after `end_time`) ```json { \"record_sources\": [ { \"record_type\": \"SPAN\", \"project_id\": \"TW9kZWw6MTIzOmFCY0Q=\", \"start_time\": \"2026-01-20T00:00:00Z\", \"end_time\": \"2026-01-15T00:00:00Z\", \"span_ids\": [\"U3BhbjoxOmFCY0Q=\"] } ] } ``` If no example_ids are provided for a dataset record source, all examples in the dataset will be added to the queue only when the total records from all sources does not exceed 500. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param annotation_queue_id: The unique annotation queue identifier (base64) (required) :type annotation_queue_id: str diff --git a/src/arize/_generated/api_client/api/api_keys_api.py b/src/arize/_generated/api_client/api/api_keys_api.py index be2673c..05c734e 100644 --- a/src/arize/_generated/api_client/api/api_keys_api.py +++ b/src/arize/_generated/api_client/api/api_keys_api.py @@ -64,7 +64,7 @@ def create_api_key( ) -> CreateApiKeyResponse: """Create an API key - Create a new API key for the authenticated user. - Choose `key_type: \"USER\"` for a personal key that authenticates as you, or `key_type: \"SERVICE\"` for an automated service account key. The field is required. - For service keys, supply at least one space via the `organizations` array. The service account is granted membership in each specified space. Multiple organizations and multiple spaces per organization are supported. - For `USER` keys, the key inherits the authenticated user's own permissions. - You may only assign roles at or below your own privilege level. Attempting to assign a role higher than your own returns `422 Unprocessable Entity`. - All roles default to minimum privilege when omitted: space roles default to `MEMBER`, organization roles default to `READ_ONLY`, and `account_role` defaults to `MEMBER`. **Authorization:** - **User keys:** Requires the `developer` user permission flag. Returns `403` when this flag is absent. - **Service keys:** Requires the `SERVICE_KEY_CREATE` permission in the target space (space member or above). The full API key value (`key`) is **only returned once** in the creation response. Store it securely — it cannot be retrieved again. Use the `redacted_key` field on subsequent reads. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Create a new API key for the authenticated user. - Choose `key_type: \"USER\"` for a personal key that authenticates as you, or `key_type: \"SERVICE\"` for an automated service account key. The field is required. - For service keys, supply at least one space via the `organizations` array. The service account is granted membership in each specified space. Multiple organizations and multiple spaces per organization are supported. - For `USER` keys, the key inherits the authenticated user's own permissions. - You may only assign roles at or below your own privilege level. Attempting to assign a role higher than your own returns `422 Unprocessable Entity`. - All roles default to minimum privilege when omitted: space roles default to `MEMBER`, organization roles default to `READ_ONLY`, and `account_role` defaults to `MEMBER`. **Authorization:** - **User keys:** The authenticated user may create personal keys for themselves. - **Service keys:** Requires the `SERVICE_KEY_CREATE` permission in the target space (space member or above). The full API key value (`key`) is **only returned once** in the creation response. Store it securely — it cannot be retrieved again. Use the `redacted_key` field on subsequent reads. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param create_api_key_request: Body containing API key creation parameters (required) :type create_api_key_request: CreateApiKeyRequest @@ -137,7 +137,7 @@ def create_api_key_with_http_info( ) -> ApiResponse[CreateApiKeyResponse]: """Create an API key - Create a new API key for the authenticated user. - Choose `key_type: \"USER\"` for a personal key that authenticates as you, or `key_type: \"SERVICE\"` for an automated service account key. The field is required. - For service keys, supply at least one space via the `organizations` array. The service account is granted membership in each specified space. Multiple organizations and multiple spaces per organization are supported. - For `USER` keys, the key inherits the authenticated user's own permissions. - You may only assign roles at or below your own privilege level. Attempting to assign a role higher than your own returns `422 Unprocessable Entity`. - All roles default to minimum privilege when omitted: space roles default to `MEMBER`, organization roles default to `READ_ONLY`, and `account_role` defaults to `MEMBER`. **Authorization:** - **User keys:** Requires the `developer` user permission flag. Returns `403` when this flag is absent. - **Service keys:** Requires the `SERVICE_KEY_CREATE` permission in the target space (space member or above). The full API key value (`key`) is **only returned once** in the creation response. Store it securely — it cannot be retrieved again. Use the `redacted_key` field on subsequent reads. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Create a new API key for the authenticated user. - Choose `key_type: \"USER\"` for a personal key that authenticates as you, or `key_type: \"SERVICE\"` for an automated service account key. The field is required. - For service keys, supply at least one space via the `organizations` array. The service account is granted membership in each specified space. Multiple organizations and multiple spaces per organization are supported. - For `USER` keys, the key inherits the authenticated user's own permissions. - You may only assign roles at or below your own privilege level. Attempting to assign a role higher than your own returns `422 Unprocessable Entity`. - All roles default to minimum privilege when omitted: space roles default to `MEMBER`, organization roles default to `READ_ONLY`, and `account_role` defaults to `MEMBER`. **Authorization:** - **User keys:** The authenticated user may create personal keys for themselves. - **Service keys:** Requires the `SERVICE_KEY_CREATE` permission in the target space (space member or above). The full API key value (`key`) is **only returned once** in the creation response. Store it securely — it cannot be retrieved again. Use the `redacted_key` field on subsequent reads. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param create_api_key_request: Body containing API key creation parameters (required) :type create_api_key_request: CreateApiKeyRequest @@ -210,7 +210,7 @@ def create_api_key_without_preload_content( ) -> RESTResponseType: """Create an API key - Create a new API key for the authenticated user. - Choose `key_type: \"USER\"` for a personal key that authenticates as you, or `key_type: \"SERVICE\"` for an automated service account key. The field is required. - For service keys, supply at least one space via the `organizations` array. The service account is granted membership in each specified space. Multiple organizations and multiple spaces per organization are supported. - For `USER` keys, the key inherits the authenticated user's own permissions. - You may only assign roles at or below your own privilege level. Attempting to assign a role higher than your own returns `422 Unprocessable Entity`. - All roles default to minimum privilege when omitted: space roles default to `MEMBER`, organization roles default to `READ_ONLY`, and `account_role` defaults to `MEMBER`. **Authorization:** - **User keys:** Requires the `developer` user permission flag. Returns `403` when this flag is absent. - **Service keys:** Requires the `SERVICE_KEY_CREATE` permission in the target space (space member or above). The full API key value (`key`) is **only returned once** in the creation response. Store it securely — it cannot be retrieved again. Use the `redacted_key` field on subsequent reads. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Create a new API key for the authenticated user. - Choose `key_type: \"USER\"` for a personal key that authenticates as you, or `key_type: \"SERVICE\"` for an automated service account key. The field is required. - For service keys, supply at least one space via the `organizations` array. The service account is granted membership in each specified space. Multiple organizations and multiple spaces per organization are supported. - For `USER` keys, the key inherits the authenticated user's own permissions. - You may only assign roles at or below your own privilege level. Attempting to assign a role higher than your own returns `422 Unprocessable Entity`. - All roles default to minimum privilege when omitted: space roles default to `MEMBER`, organization roles default to `READ_ONLY`, and `account_role` defaults to `MEMBER`. **Authorization:** - **User keys:** The authenticated user may create personal keys for themselves. - **Service keys:** Requires the `SERVICE_KEY_CREATE` permission in the target space (space member or above). The full API key value (`key`) is **only returned once** in the creation response. Store it securely — it cannot be retrieved again. Use the `redacted_key` field on subsequent reads. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param create_api_key_request: Body containing API key creation parameters (required) :type create_api_key_request: CreateApiKeyRequest @@ -362,7 +362,7 @@ def list_api_keys( ) -> ListApiKeysResponse: """List API keys - List API keys. Returns metadata for each key (id, name, description, key_type, status, redacted_key, created_at, expires_at, created_by_user_id). The raw key secret is never returned after creation. Results can be filtered by key type, status, space, and creator. Responses are paginated; use `limit` and `cursor` and the response `pagination.next_cursor` for subsequent pages. **Service keys (`key_type=SERVICE`):** Provide `space_id` to return all service keys for that space. When `key_type` is omitted alongside `space_id`, service keys are returned implicitly. Requires the `SERVICE_KEY_READ` permission in the space (or account/space admin). Optionally combine with `user_id` to filter service keys by their creator — available to any caller with space access (not admin-gated). **User keys (`key_type=USER`):** Returned by default (no `space_id`). Provide `user_id` to view keys belonging to a specific user — account admins only; non-admins receive `403`. **Authorization:** Requires the `developer` user permission flag or account admin role. Returns `403` when neither condition is met. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + List API keys. Returns metadata for each key (id, name, description, key_type, status, redacted_key, created_at, expires_at, created_by_user_id). The raw key secret is never returned after creation. Results can be filtered by key type, status, space, and creator. Responses are paginated; use `limit` and `cursor` and the response `pagination.next_cursor` for subsequent pages. **Service keys (`key_type=SERVICE`):** Provide `space_id` to return all service keys for that space. When `key_type` is omitted alongside `space_id`, service keys are returned implicitly. Requires the `SERVICE_KEY_READ` permission in the space (or account/space admin). Optionally combine with `user_id` to filter service keys by their creator — available to any caller with space access (not admin-gated). **User keys (`key_type=USER`):** Returned by default (no `space_id`). Provide `user_id` to view keys belonging to a specific user — account admins only; non-admins receive `403`. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param key_type: Filter by API key type. - USER - Key associated with a specific user. - SERVICE - Key associated with a bot user for service authentication. :type key_type: ApiKeyType @@ -454,7 +454,7 @@ def list_api_keys_with_http_info( ) -> ApiResponse[ListApiKeysResponse]: """List API keys - List API keys. Returns metadata for each key (id, name, description, key_type, status, redacted_key, created_at, expires_at, created_by_user_id). The raw key secret is never returned after creation. Results can be filtered by key type, status, space, and creator. Responses are paginated; use `limit` and `cursor` and the response `pagination.next_cursor` for subsequent pages. **Service keys (`key_type=SERVICE`):** Provide `space_id` to return all service keys for that space. When `key_type` is omitted alongside `space_id`, service keys are returned implicitly. Requires the `SERVICE_KEY_READ` permission in the space (or account/space admin). Optionally combine with `user_id` to filter service keys by their creator — available to any caller with space access (not admin-gated). **User keys (`key_type=USER`):** Returned by default (no `space_id`). Provide `user_id` to view keys belonging to a specific user — account admins only; non-admins receive `403`. **Authorization:** Requires the `developer` user permission flag or account admin role. Returns `403` when neither condition is met. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + List API keys. Returns metadata for each key (id, name, description, key_type, status, redacted_key, created_at, expires_at, created_by_user_id). The raw key secret is never returned after creation. Results can be filtered by key type, status, space, and creator. Responses are paginated; use `limit` and `cursor` and the response `pagination.next_cursor` for subsequent pages. **Service keys (`key_type=SERVICE`):** Provide `space_id` to return all service keys for that space. When `key_type` is omitted alongside `space_id`, service keys are returned implicitly. Requires the `SERVICE_KEY_READ` permission in the space (or account/space admin). Optionally combine with `user_id` to filter service keys by their creator — available to any caller with space access (not admin-gated). **User keys (`key_type=USER`):** Returned by default (no `space_id`). Provide `user_id` to view keys belonging to a specific user — account admins only; non-admins receive `403`. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param key_type: Filter by API key type. - USER - Key associated with a specific user. - SERVICE - Key associated with a bot user for service authentication. :type key_type: ApiKeyType @@ -546,7 +546,7 @@ def list_api_keys_without_preload_content( ) -> RESTResponseType: """List API keys - List API keys. Returns metadata for each key (id, name, description, key_type, status, redacted_key, created_at, expires_at, created_by_user_id). The raw key secret is never returned after creation. Results can be filtered by key type, status, space, and creator. Responses are paginated; use `limit` and `cursor` and the response `pagination.next_cursor` for subsequent pages. **Service keys (`key_type=SERVICE`):** Provide `space_id` to return all service keys for that space. When `key_type` is omitted alongside `space_id`, service keys are returned implicitly. Requires the `SERVICE_KEY_READ` permission in the space (or account/space admin). Optionally combine with `user_id` to filter service keys by their creator — available to any caller with space access (not admin-gated). **User keys (`key_type=USER`):** Returned by default (no `space_id`). Provide `user_id` to view keys belonging to a specific user — account admins only; non-admins receive `403`. **Authorization:** Requires the `developer` user permission flag or account admin role. Returns `403` when neither condition is met. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + List API keys. Returns metadata for each key (id, name, description, key_type, status, redacted_key, created_at, expires_at, created_by_user_id). The raw key secret is never returned after creation. Results can be filtered by key type, status, space, and creator. Responses are paginated; use `limit` and `cursor` and the response `pagination.next_cursor` for subsequent pages. **Service keys (`key_type=SERVICE`):** Provide `space_id` to return all service keys for that space. When `key_type` is omitted alongside `space_id`, service keys are returned implicitly. Requires the `SERVICE_KEY_READ` permission in the space (or account/space admin). Optionally combine with `user_id` to filter service keys by their creator — available to any caller with space access (not admin-gated). **User keys (`key_type=USER`):** Returned by default (no `space_id`). Provide `user_id` to view keys belonging to a specific user — account admins only; non-admins receive `403`. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param key_type: Filter by API key type. - USER - Key associated with a specific user. - SERVICE - Key associated with a bot user for service authentication. :type key_type: ApiKeyType @@ -722,7 +722,7 @@ def refresh_api_key( ) -> RefreshApiKeyResponse: """Refresh an API key - Atomically revoke an existing API key and issue a replacement with the same metadata (name, description, and key type). The old key is invalidated and the new key is activated in a single transaction — there is no window where neither key is valid. The full new key value (`key`) is **only returned once** in the response. Store it securely. **Authorization:** - **User keys:** the creator or an account admin may refresh the key. Requires the `developer` user permission flag. Returns `403` when this flag is absent. - **Service keys:** space admins (and higher) may refresh any service key in their space. Non-admins require the `SERVICE_KEY_CREATE` permission and must be the creator of the key. **Expiry behaviour:** `expires_at` is **required** when the existing key has an expiry — omitting it would extend the key's lifetime to unbounded and is rejected with `422`. For unbounded existing keys, `expires_at` may be omitted (the replacement is also unbounded) or supplied to add a specific expiry. The value must not be later than the existing key's expiry; to issue a key with a longer lifetime, use `POST /v2/api-keys`. **Grace period:** Supply `grace_period_seconds` in the request body to keep the old key valid for that many seconds after the refresh. If not supplied, the old key is revoked immediately. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Atomically revoke an existing API key and issue a replacement with the same metadata (name, description, and key type). The old key is invalidated and the new key is activated in a single transaction — there is no window where neither key is valid. The full new key value (`key`) is **only returned once** in the response. Store it securely. **Authorization:** - **User keys:** The creator or an account admin may refresh the key. - **Service keys:** space admins (and higher) may refresh any service key in their space. Non-admins require the `SERVICE_KEY_CREATE` permission and must be the creator of the key. **Expiry behaviour:** `expires_at` is **required** when the existing key has an expiry — omitting it would extend the key's lifetime to unbounded and is rejected with `422`. For unbounded existing keys, `expires_at` may be omitted (the replacement is also unbounded) or supplied to add a specific expiry. The value must not be later than the existing key's expiry; to issue a key with a longer lifetime, use `POST /v2/api-keys`. **Grace period:** Supply `grace_period_seconds` in the request body to keep the old key valid for that many seconds after the refresh. If not supplied, the old key is revoked immediately. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param api_key_id: The unique API key identifier (base64) (required) :type api_key_id: str @@ -799,7 +799,7 @@ def refresh_api_key_with_http_info( ) -> ApiResponse[RefreshApiKeyResponse]: """Refresh an API key - Atomically revoke an existing API key and issue a replacement with the same metadata (name, description, and key type). The old key is invalidated and the new key is activated in a single transaction — there is no window where neither key is valid. The full new key value (`key`) is **only returned once** in the response. Store it securely. **Authorization:** - **User keys:** the creator or an account admin may refresh the key. Requires the `developer` user permission flag. Returns `403` when this flag is absent. - **Service keys:** space admins (and higher) may refresh any service key in their space. Non-admins require the `SERVICE_KEY_CREATE` permission and must be the creator of the key. **Expiry behaviour:** `expires_at` is **required** when the existing key has an expiry — omitting it would extend the key's lifetime to unbounded and is rejected with `422`. For unbounded existing keys, `expires_at` may be omitted (the replacement is also unbounded) or supplied to add a specific expiry. The value must not be later than the existing key's expiry; to issue a key with a longer lifetime, use `POST /v2/api-keys`. **Grace period:** Supply `grace_period_seconds` in the request body to keep the old key valid for that many seconds after the refresh. If not supplied, the old key is revoked immediately. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Atomically revoke an existing API key and issue a replacement with the same metadata (name, description, and key type). The old key is invalidated and the new key is activated in a single transaction — there is no window where neither key is valid. The full new key value (`key`) is **only returned once** in the response. Store it securely. **Authorization:** - **User keys:** The creator or an account admin may refresh the key. - **Service keys:** space admins (and higher) may refresh any service key in their space. Non-admins require the `SERVICE_KEY_CREATE` permission and must be the creator of the key. **Expiry behaviour:** `expires_at` is **required** when the existing key has an expiry — omitting it would extend the key's lifetime to unbounded and is rejected with `422`. For unbounded existing keys, `expires_at` may be omitted (the replacement is also unbounded) or supplied to add a specific expiry. The value must not be later than the existing key's expiry; to issue a key with a longer lifetime, use `POST /v2/api-keys`. **Grace period:** Supply `grace_period_seconds` in the request body to keep the old key valid for that many seconds after the refresh. If not supplied, the old key is revoked immediately. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param api_key_id: The unique API key identifier (base64) (required) :type api_key_id: str @@ -876,7 +876,7 @@ def refresh_api_key_without_preload_content( ) -> RESTResponseType: """Refresh an API key - Atomically revoke an existing API key and issue a replacement with the same metadata (name, description, and key type). The old key is invalidated and the new key is activated in a single transaction — there is no window where neither key is valid. The full new key value (`key`) is **only returned once** in the response. Store it securely. **Authorization:** - **User keys:** the creator or an account admin may refresh the key. Requires the `developer` user permission flag. Returns `403` when this flag is absent. - **Service keys:** space admins (and higher) may refresh any service key in their space. Non-admins require the `SERVICE_KEY_CREATE` permission and must be the creator of the key. **Expiry behaviour:** `expires_at` is **required** when the existing key has an expiry — omitting it would extend the key's lifetime to unbounded and is rejected with `422`. For unbounded existing keys, `expires_at` may be omitted (the replacement is also unbounded) or supplied to add a specific expiry. The value must not be later than the existing key's expiry; to issue a key with a longer lifetime, use `POST /v2/api-keys`. **Grace period:** Supply `grace_period_seconds` in the request body to keep the old key valid for that many seconds after the refresh. If not supplied, the old key is revoked immediately. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Atomically revoke an existing API key and issue a replacement with the same metadata (name, description, and key type). The old key is invalidated and the new key is activated in a single transaction — there is no window where neither key is valid. The full new key value (`key`) is **only returned once** in the response. Store it securely. **Authorization:** - **User keys:** The creator or an account admin may refresh the key. - **Service keys:** space admins (and higher) may refresh any service key in their space. Non-admins require the `SERVICE_KEY_CREATE` permission and must be the creator of the key. **Expiry behaviour:** `expires_at` is **required** when the existing key has an expiry — omitting it would extend the key's lifetime to unbounded and is rejected with `422`. For unbounded existing keys, `expires_at` may be omitted (the replacement is also unbounded) or supplied to add a specific expiry. The value must not be later than the existing key's expiry; to issue a key with a longer lifetime, use `POST /v2/api-keys`. **Grace period:** Supply `grace_period_seconds` in the request body to keep the old key valid for that many seconds after the refresh. If not supplied, the old key is revoked immediately. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param api_key_id: The unique API key identifier (base64) (required) :type api_key_id: str @@ -1029,7 +1029,7 @@ def revoke_api_key( ) -> None: """Revoke an API key - Revoke an API key by its ID. The key will immediately stop working for authentication. Revoking an already-revoked key is a no-op and still returns `204`. **Authorization:** Requires the `developer` user permission flag **or** account admin role (either condition is sufficient). Returns `403` when neither condition is met. For service keys, only the key's creator or an account admin may revoke the key. A developer who did not create the key receives `404` (prevents key-ID enumeration). This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Revoke an API key by its ID. The key will immediately stop working for authentication. Revoking an already-revoked key is a no-op and still returns `204`. **Authorization:** - **User keys:** The key's creator or an account admin may revoke the key. - **Service keys:** Account admins and space admins in all of the key's bound spaces may revoke the key regardless of who created it. All other callers must have the `SERVICE_KEY_REVOKE` permission in every bound space and must be the key's creator. Callers without read access to the key receive `404` to prevent key-ID enumeration. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param api_key_id: The unique API key identifier (base64) (required) :type api_key_id: str @@ -1101,7 +1101,7 @@ def revoke_api_key_with_http_info( ) -> ApiResponse[None]: """Revoke an API key - Revoke an API key by its ID. The key will immediately stop working for authentication. Revoking an already-revoked key is a no-op and still returns `204`. **Authorization:** Requires the `developer` user permission flag **or** account admin role (either condition is sufficient). Returns `403` when neither condition is met. For service keys, only the key's creator or an account admin may revoke the key. A developer who did not create the key receives `404` (prevents key-ID enumeration). This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Revoke an API key by its ID. The key will immediately stop working for authentication. Revoking an already-revoked key is a no-op and still returns `204`. **Authorization:** - **User keys:** The key's creator or an account admin may revoke the key. - **Service keys:** Account admins and space admins in all of the key's bound spaces may revoke the key regardless of who created it. All other callers must have the `SERVICE_KEY_REVOKE` permission in every bound space and must be the key's creator. Callers without read access to the key receive `404` to prevent key-ID enumeration. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param api_key_id: The unique API key identifier (base64) (required) :type api_key_id: str @@ -1173,7 +1173,7 @@ def revoke_api_key_without_preload_content( ) -> RESTResponseType: """Revoke an API key - Revoke an API key by its ID. The key will immediately stop working for authentication. Revoking an already-revoked key is a no-op and still returns `204`. **Authorization:** Requires the `developer` user permission flag **or** account admin role (either condition is sufficient). Returns `403` when neither condition is met. For service keys, only the key's creator or an account admin may revoke the key. A developer who did not create the key receives `404` (prevents key-ID enumeration). This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Revoke an API key by its ID. The key will immediately stop working for authentication. Revoking an already-revoked key is a no-op and still returns `204`. **Authorization:** - **User keys:** The key's creator or an account admin may revoke the key. - **Service keys:** Account admins and space admins in all of the key's bound spaces may revoke the key regardless of who created it. All other callers must have the `SERVICE_KEY_REVOKE` permission in every bound space and must be the key's creator. Callers without read access to the key receive `404` to prevent key-ID enumeration. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param api_key_id: The unique API key identifier (base64) (required) :type api_key_id: str diff --git a/src/arize/_generated/api_client/api/experiments_api.py b/src/arize/_generated/api_client/api/experiments_api.py index 8906574..e39b722 100644 --- a/src/arize/_generated/api_client/api/experiments_api.py +++ b/src/arize/_generated/api_client/api/experiments_api.py @@ -371,7 +371,7 @@ def create_experiment( ) -> Experiment: """Create an experiment - Create a new experiment. Empty experiments are not allowed. Experiments are composed of \"runs\". Each experiment run (JSON object) must include an `example_id` field that corresponds to an example in the dataset, and a `output` field that contains the task's output for the example (the input). Payload Requirements - The `name` must be unique within the target dataset - Provide at least one run in `experiment_runs`. - Each run must include: - `example_id` -- the ID of an existing example in the dataset/version - `output` -- model/task output for that example - You may include any additional fields per run that can be used for analysis or filtering. For exampple: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Create a new experiment. Empty experiments are not allowed. An experiment belongs to a space and may optionally be associated with a dataset. Experiments are composed of \"runs\". Each experiment run (JSON object) must include an `output` field containing the task's output. When the experiment is associated with a dataset, each run must also include an `example_id` referencing an example in that dataset. Payload Requirements - Provide exactly one of `dataset_id` or `space_id`. - The `name` must be unique within the dataset it's associated with, or within the space when it isn't associated with a dataset. - Provide at least one run in `experiment_runs`. - Each run must include: - `output` -- model/task output for the run - `example_id` -- the ID of an existing example in the dataset, required only when the experiment is associated with a dataset - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param create_experiment_request: Body containing experiment creation parameters (required) :type create_experiment_request: CreateExperimentRequest @@ -445,7 +445,7 @@ def create_experiment_with_http_info( ) -> ApiResponse[Experiment]: """Create an experiment - Create a new experiment. Empty experiments are not allowed. Experiments are composed of \"runs\". Each experiment run (JSON object) must include an `example_id` field that corresponds to an example in the dataset, and a `output` field that contains the task's output for the example (the input). Payload Requirements - The `name` must be unique within the target dataset - Provide at least one run in `experiment_runs`. - Each run must include: - `example_id` -- the ID of an existing example in the dataset/version - `output` -- model/task output for that example - You may include any additional fields per run that can be used for analysis or filtering. For exampple: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Create a new experiment. Empty experiments are not allowed. An experiment belongs to a space and may optionally be associated with a dataset. Experiments are composed of \"runs\". Each experiment run (JSON object) must include an `output` field containing the task's output. When the experiment is associated with a dataset, each run must also include an `example_id` referencing an example in that dataset. Payload Requirements - Provide exactly one of `dataset_id` or `space_id`. - The `name` must be unique within the dataset it's associated with, or within the space when it isn't associated with a dataset. - Provide at least one run in `experiment_runs`. - Each run must include: - `output` -- model/task output for the run - `example_id` -- the ID of an existing example in the dataset, required only when the experiment is associated with a dataset - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param create_experiment_request: Body containing experiment creation parameters (required) :type create_experiment_request: CreateExperimentRequest @@ -519,7 +519,7 @@ def create_experiment_without_preload_content( ) -> RESTResponseType: """Create an experiment - Create a new experiment. Empty experiments are not allowed. Experiments are composed of \"runs\". Each experiment run (JSON object) must include an `example_id` field that corresponds to an example in the dataset, and a `output` field that contains the task's output for the example (the input). Payload Requirements - The `name` must be unique within the target dataset - Provide at least one run in `experiment_runs`. - Each run must include: - `example_id` -- the ID of an existing example in the dataset/version - `output` -- model/task output for that example - You may include any additional fields per run that can be used for analysis or filtering. For exampple: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Create a new experiment. Empty experiments are not allowed. An experiment belongs to a space and may optionally be associated with a dataset. Experiments are composed of \"runs\". Each experiment run (JSON object) must include an `output` field containing the task's output. When the experiment is associated with a dataset, each run must also include an `example_id` referencing an example in that dataset. Payload Requirements - Provide exactly one of `dataset_id` or `space_id`. - The `name` must be unique within the dataset it's associated with, or within the space when it isn't associated with a dataset. - Provide at least one run in `experiment_runs`. - Each run must include: - `output` -- model/task output for the run - `example_id` -- the ID of an existing example in the dataset, required only when the experiment is associated with a dataset - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param create_experiment_request: Body containing experiment creation parameters (required) :type create_experiment_request: CreateExperimentRequest @@ -1218,7 +1218,7 @@ def insert_experiment_runs( ) -> ExperimentWithRunIds: """Append runs to an experiment - Append new runs to an existing experiment. **Payload Requirements** - Provide between 1 and 1000 runs in `experiment_runs`. - Each run must include: - `example_id` -- the ID of an existing example in the dataset version - `output` -- model/task output for that example - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. **Valid example** ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\", \"output\": \"4\", \"model\": \"gpt-4o-mini\"} ] } ``` **Invalid example** (missing required output field) ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\"} ] } ``` This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Append new runs to an existing experiment. **Payload Requirements** - Provide between 1 and 1000 runs in `experiment_runs`. - Each run must include: - `output` -- model/task output for the run - `example_id` -- the ID of an existing example in the dataset, required only when the experiment is associated with a dataset - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. **Valid example** ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\", \"output\": \"4\", \"model\": \"gpt-4o-mini\"} ] } ``` **Invalid example** (missing required output field) ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\"} ] } ``` This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param experiment_id: The unique experiment identifier (base64) (required) :type experiment_id: str @@ -1295,7 +1295,7 @@ def insert_experiment_runs_with_http_info( ) -> ApiResponse[ExperimentWithRunIds]: """Append runs to an experiment - Append new runs to an existing experiment. **Payload Requirements** - Provide between 1 and 1000 runs in `experiment_runs`. - Each run must include: - `example_id` -- the ID of an existing example in the dataset version - `output` -- model/task output for that example - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. **Valid example** ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\", \"output\": \"4\", \"model\": \"gpt-4o-mini\"} ] } ``` **Invalid example** (missing required output field) ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\"} ] } ``` This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Append new runs to an existing experiment. **Payload Requirements** - Provide between 1 and 1000 runs in `experiment_runs`. - Each run must include: - `output` -- model/task output for the run - `example_id` -- the ID of an existing example in the dataset, required only when the experiment is associated with a dataset - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. **Valid example** ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\", \"output\": \"4\", \"model\": \"gpt-4o-mini\"} ] } ``` **Invalid example** (missing required output field) ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\"} ] } ``` This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param experiment_id: The unique experiment identifier (base64) (required) :type experiment_id: str @@ -1372,7 +1372,7 @@ def insert_experiment_runs_without_preload_content( ) -> RESTResponseType: """Append runs to an experiment - Append new runs to an existing experiment. **Payload Requirements** - Provide between 1 and 1000 runs in `experiment_runs`. - Each run must include: - `example_id` -- the ID of an existing example in the dataset version - `output` -- model/task output for that example - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. **Valid example** ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\", \"output\": \"4\", \"model\": \"gpt-4o-mini\"} ] } ``` **Invalid example** (missing required output field) ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\"} ] } ``` This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Append new runs to an existing experiment. **Payload Requirements** - Provide between 1 and 1000 runs in `experiment_runs`. - Each run must include: - `output` -- model/task output for the run - `example_id` -- the ID of an existing example in the dataset, required only when the experiment is associated with a dataset - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. **Valid example** ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\", \"output\": \"4\", \"model\": \"gpt-4o-mini\"} ] } ``` **Invalid example** (missing required output field) ```json { \"experiment_runs\": [ {\"example_id\": \"example_001\"} ] } ``` This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param experiment_id: The unique experiment identifier (base64) (required) :type experiment_id: str @@ -1821,6 +1821,7 @@ def _list_experiment_runs_serialize( def list_experiments( self, dataset_id: Annotated[Optional[StrictStr], Field(description="Filter to a specific dataset (base64 identifier (base64))")] = None, + space_id: Annotated[Optional[StrictStr], Field(description="Filter search results to a particular space ID")] = None, name: Annotated[Optional[Annotated[str, Field(strict=True, max_length=255)]], Field(description="Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. ")] = None, limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum items to return")] = None, cursor: Annotated[Optional[StrictStr], Field(description="Opaque pagination cursor returned from a previous response (`pagination.next_cursor`). Treat it as an unreadable token; do not attempt to parse or construct it. ")] = None, @@ -1839,10 +1840,12 @@ def list_experiments( ) -> ListExperimentsResponse: """List experiments - List all experiments a user has access to. To filter experiments by the dataset they were run on, provide the `dataset_id` query parameter. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + List experiments a user has access to. By default, lists every accessible experiment across all spaces the caller can read, including experiments that are not associated with a dataset. To narrow the results, provide at most one of: - `dataset_id` — only experiments run on that dataset. - `space_id` — only experiments in that space (with or without a dataset). Providing both `dataset_id` and `space_id` is a validation error. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param dataset_id: Filter to a specific dataset (base64 identifier (base64)) :type dataset_id: str + :param space_id: Filter search results to a particular space ID + :type space_id: str :param name: Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. :type name: str :param limit: Maximum items to return @@ -1873,6 +1876,7 @@ def list_experiments( _param = self._list_experiments_serialize( dataset_id=dataset_id, + space_id=space_id, name=name, limit=limit, cursor=cursor, @@ -1905,6 +1909,7 @@ def list_experiments( def list_experiments_with_http_info( self, dataset_id: Annotated[Optional[StrictStr], Field(description="Filter to a specific dataset (base64 identifier (base64))")] = None, + space_id: Annotated[Optional[StrictStr], Field(description="Filter search results to a particular space ID")] = None, name: Annotated[Optional[Annotated[str, Field(strict=True, max_length=255)]], Field(description="Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. ")] = None, limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum items to return")] = None, cursor: Annotated[Optional[StrictStr], Field(description="Opaque pagination cursor returned from a previous response (`pagination.next_cursor`). Treat it as an unreadable token; do not attempt to parse or construct it. ")] = None, @@ -1923,10 +1928,12 @@ def list_experiments_with_http_info( ) -> ApiResponse[ListExperimentsResponse]: """List experiments - List all experiments a user has access to. To filter experiments by the dataset they were run on, provide the `dataset_id` query parameter. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + List experiments a user has access to. By default, lists every accessible experiment across all spaces the caller can read, including experiments that are not associated with a dataset. To narrow the results, provide at most one of: - `dataset_id` — only experiments run on that dataset. - `space_id` — only experiments in that space (with or without a dataset). Providing both `dataset_id` and `space_id` is a validation error. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param dataset_id: Filter to a specific dataset (base64 identifier (base64)) :type dataset_id: str + :param space_id: Filter search results to a particular space ID + :type space_id: str :param name: Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. :type name: str :param limit: Maximum items to return @@ -1957,6 +1964,7 @@ def list_experiments_with_http_info( _param = self._list_experiments_serialize( dataset_id=dataset_id, + space_id=space_id, name=name, limit=limit, cursor=cursor, @@ -1989,6 +1997,7 @@ def list_experiments_with_http_info( def list_experiments_without_preload_content( self, dataset_id: Annotated[Optional[StrictStr], Field(description="Filter to a specific dataset (base64 identifier (base64))")] = None, + space_id: Annotated[Optional[StrictStr], Field(description="Filter search results to a particular space ID")] = None, name: Annotated[Optional[Annotated[str, Field(strict=True, max_length=255)]], Field(description="Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. ")] = None, limit: Annotated[Optional[Annotated[int, Field(le=100, strict=True, ge=1)]], Field(description="Maximum items to return")] = None, cursor: Annotated[Optional[StrictStr], Field(description="Opaque pagination cursor returned from a previous response (`pagination.next_cursor`). Treat it as an unreadable token; do not attempt to parse or construct it. ")] = None, @@ -2007,10 +2016,12 @@ def list_experiments_without_preload_content( ) -> RESTResponseType: """List experiments - List all experiments a user has access to. To filter experiments by the dataset they were run on, provide the `dataset_id` query parameter. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + List experiments a user has access to. By default, lists every accessible experiment across all spaces the caller can read, including experiments that are not associated with a dataset. To narrow the results, provide at most one of: - `dataset_id` — only experiments run on that dataset. - `space_id` — only experiments in that space (with or without a dataset). Providing both `dataset_id` and `space_id` is a validation error. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param dataset_id: Filter to a specific dataset (base64 identifier (base64)) :type dataset_id: str + :param space_id: Filter search results to a particular space ID + :type space_id: str :param name: Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. :type name: str :param limit: Maximum items to return @@ -2041,6 +2052,7 @@ def list_experiments_without_preload_content( _param = self._list_experiments_serialize( dataset_id=dataset_id, + space_id=space_id, name=name, limit=limit, cursor=cursor, @@ -2068,6 +2080,7 @@ def list_experiments_without_preload_content( def _list_experiments_serialize( self, dataset_id, + space_id, name, limit, cursor, @@ -2097,6 +2110,10 @@ def _list_experiments_serialize( _query_params.append(('dataset_id', dataset_id)) + if space_id is not None: + + _query_params.append(('space_id', space_id)) + if name is not None: _query_params.append(('name', name)) diff --git a/src/arize/_generated/api_client/api/integrations_api.py b/src/arize/_generated/api_client/api/integrations_api.py index fdfb5a6..c457adf 100644 --- a/src/arize/_generated/api_client/api/integrations_api.py +++ b/src/arize/_generated/api_client/api/integrations_api.py @@ -892,7 +892,7 @@ def _get_integration_serialize( @validate_call def list_integrations( self, - type: Annotated[IntegrationType, Field(description="The integration type to list. Required - the list returns only integrations of this type.")], + type: Annotated[Optional[IntegrationType], Field(description="Filter the list to a single integration type. When omitted, integrations of every type are returned; each item carries its `type` for client-side discrimination.")] = None, space_id: Annotated[Optional[StrictStr], Field(description="Filter search results to a particular space ID")] = None, space_name: Annotated[Optional[Annotated[str, Field(strict=True, max_length=255)]], Field(description="Case-insensitive substring filter on the space name. Narrows results to resources in spaces whose name contains the given string. If omitted, no space name filtering is applied and all resources are returned. ")] = None, name: Annotated[Optional[Annotated[str, Field(strict=True, max_length=255)]], Field(description="Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. ")] = None, @@ -913,9 +913,9 @@ def list_integrations( ) -> ListIntegrationsResponse: """List integrations - List integrations the user has access to. `type` is required and the response contains only integrations of that type. Each item still carries its `type` (and, for `LLM`, `config.provider`) for client-side discrimination. A missing or invalid `type` returns `400`. Integrations are owned at the account level but carry visibility scopings (account-wide, organization, or space). `space_id` / `space_name` filter the list to integrations visible in a given space. This endpoint is in alpha, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + List integrations the user has access to, ordered by creation time (newest first). By default the list includes every integration type; pass `type` to list a single type. Each item carries its `type` (and, for `LLM`, `config.provider`) for client-side discrimination. An invalid `type` or pagination `cursor` returns `400`; a cursor is only valid for the query parameters it was issued with. Integrations are owned at the account level but carry visibility scopings (account-wide, organization, or space). `space_id` / `space_name` filter the list to integrations visible in a given space. The list contains only the types the caller has permission to read. When no type is readable the request fails with `403` — or `404` when a `space_id` filter references a space outside the caller's visibility. This endpoint is in alpha, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). - :param type: The integration type to list. Required - the list returns only integrations of this type. (required) + :param type: Filter the list to a single integration type. When omitted, integrations of every type are returned; each item carries its `type` for client-side discrimination. :type type: IntegrationType :param space_id: Filter search results to a particular space ID :type space_id: str @@ -984,7 +984,7 @@ def list_integrations( @validate_call def list_integrations_with_http_info( self, - type: Annotated[IntegrationType, Field(description="The integration type to list. Required - the list returns only integrations of this type.")], + type: Annotated[Optional[IntegrationType], Field(description="Filter the list to a single integration type. When omitted, integrations of every type are returned; each item carries its `type` for client-side discrimination.")] = None, space_id: Annotated[Optional[StrictStr], Field(description="Filter search results to a particular space ID")] = None, space_name: Annotated[Optional[Annotated[str, Field(strict=True, max_length=255)]], Field(description="Case-insensitive substring filter on the space name. Narrows results to resources in spaces whose name contains the given string. If omitted, no space name filtering is applied and all resources are returned. ")] = None, name: Annotated[Optional[Annotated[str, Field(strict=True, max_length=255)]], Field(description="Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. ")] = None, @@ -1005,9 +1005,9 @@ def list_integrations_with_http_info( ) -> ApiResponse[ListIntegrationsResponse]: """List integrations - List integrations the user has access to. `type` is required and the response contains only integrations of that type. Each item still carries its `type` (and, for `LLM`, `config.provider`) for client-side discrimination. A missing or invalid `type` returns `400`. Integrations are owned at the account level but carry visibility scopings (account-wide, organization, or space). `space_id` / `space_name` filter the list to integrations visible in a given space. This endpoint is in alpha, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + List integrations the user has access to, ordered by creation time (newest first). By default the list includes every integration type; pass `type` to list a single type. Each item carries its `type` (and, for `LLM`, `config.provider`) for client-side discrimination. An invalid `type` or pagination `cursor` returns `400`; a cursor is only valid for the query parameters it was issued with. Integrations are owned at the account level but carry visibility scopings (account-wide, organization, or space). `space_id` / `space_name` filter the list to integrations visible in a given space. The list contains only the types the caller has permission to read. When no type is readable the request fails with `403` — or `404` when a `space_id` filter references a space outside the caller's visibility. This endpoint is in alpha, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). - :param type: The integration type to list. Required - the list returns only integrations of this type. (required) + :param type: Filter the list to a single integration type. When omitted, integrations of every type are returned; each item carries its `type` for client-side discrimination. :type type: IntegrationType :param space_id: Filter search results to a particular space ID :type space_id: str @@ -1076,7 +1076,7 @@ def list_integrations_with_http_info( @validate_call def list_integrations_without_preload_content( self, - type: Annotated[IntegrationType, Field(description="The integration type to list. Required - the list returns only integrations of this type.")], + type: Annotated[Optional[IntegrationType], Field(description="Filter the list to a single integration type. When omitted, integrations of every type are returned; each item carries its `type` for client-side discrimination.")] = None, space_id: Annotated[Optional[StrictStr], Field(description="Filter search results to a particular space ID")] = None, space_name: Annotated[Optional[Annotated[str, Field(strict=True, max_length=255)]], Field(description="Case-insensitive substring filter on the space name. Narrows results to resources in spaces whose name contains the given string. If omitted, no space name filtering is applied and all resources are returned. ")] = None, name: Annotated[Optional[Annotated[str, Field(strict=True, max_length=255)]], Field(description="Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. ")] = None, @@ -1097,9 +1097,9 @@ def list_integrations_without_preload_content( ) -> RESTResponseType: """List integrations - List integrations the user has access to. `type` is required and the response contains only integrations of that type. Each item still carries its `type` (and, for `LLM`, `config.provider`) for client-side discrimination. A missing or invalid `type` returns `400`. Integrations are owned at the account level but carry visibility scopings (account-wide, organization, or space). `space_id` / `space_name` filter the list to integrations visible in a given space. This endpoint is in alpha, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + List integrations the user has access to, ordered by creation time (newest first). By default the list includes every integration type; pass `type` to list a single type. Each item carries its `type` (and, for `LLM`, `config.provider`) for client-side discrimination. An invalid `type` or pagination `cursor` returns `400`; a cursor is only valid for the query parameters it was issued with. Integrations are owned at the account level but carry visibility scopings (account-wide, organization, or space). `space_id` / `space_name` filter the list to integrations visible in a given space. The list contains only the types the caller has permission to read. When no type is readable the request fails with `403` — or `404` when a `space_id` filter references a space outside the caller's visibility. This endpoint is in alpha, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). - :param type: The integration type to list. Required - the list returns only integrations of this type. (required) + :param type: Filter the list to a single integration type. When omitted, integrations of every type are returned; each item carries its `type` for client-side discrimination. :type type: IntegrationType :param space_id: Filter search results to a particular space ID :type space_id: str diff --git a/src/arize/_generated/api_client/api/spans_api.py b/src/arize/_generated/api_client/api/spans_api.py index 6bb9f1d..a97fd33 100644 --- a/src/arize/_generated/api_client/api/spans_api.py +++ b/src/arize/_generated/api_client/api/spans_api.py @@ -354,7 +354,7 @@ def delete_spans( ) -> DeleteSpansResponse: """Delete spans - Permanently deletes spans by their span IDs. This operation is irreversible. Accepts between 1 and 5000 span IDs per request. Only spans within the supported time range (2 years) are considered; older spans are not affected. A `200 OK` response always includes: - `completed` — `true` if the operation finished and no retry is needed; `false` if the operation could not fully complete (retry the full request). - `deleted_span_ids` — span IDs confirmed deleted in this request. - `not_deleted_span_ids` — requested IDs not deleted: either not found within the supported time range, or not reached when `completed` is `false`. The delete operation is idempotent — re-submitting already-deleted IDs is safe. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Permanently deletes spans by their span IDs. This operation is irreversible. Accepts between 1 and 5000 span IDs per request. Only spans within the searched time window are considered; spans outside that window are not affected. The optional `start_time` and `end_time` fields scope the search to a specific time window. Each bound is independent: omitting `start_time` defaults to two years ago; omitting `end_time` defaults to now. You may provide either or both. Providing them when the approximate timestamp of the target spans is known significantly reduces the amount of span data the server must search. A `200 OK` response always includes: - `completed` — `true` if the operation finished and no retry is needed; `false` if the operation could not fully complete (retry the full request). - `deleted_span_ids` — span IDs confirmed deleted in this request. - `not_deleted_span_ids` — requested IDs not deleted: either not found within the supported time range, or not reached when `completed` is `false`. The delete operation is idempotent — re-submitting already-deleted IDs is safe. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param delete_spans_request: Body containing span IDs to delete (required) :type delete_spans_request: DeleteSpansRequest @@ -428,7 +428,7 @@ def delete_spans_with_http_info( ) -> ApiResponse[DeleteSpansResponse]: """Delete spans - Permanently deletes spans by their span IDs. This operation is irreversible. Accepts between 1 and 5000 span IDs per request. Only spans within the supported time range (2 years) are considered; older spans are not affected. A `200 OK` response always includes: - `completed` — `true` if the operation finished and no retry is needed; `false` if the operation could not fully complete (retry the full request). - `deleted_span_ids` — span IDs confirmed deleted in this request. - `not_deleted_span_ids` — requested IDs not deleted: either not found within the supported time range, or not reached when `completed` is `false`. The delete operation is idempotent — re-submitting already-deleted IDs is safe. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Permanently deletes spans by their span IDs. This operation is irreversible. Accepts between 1 and 5000 span IDs per request. Only spans within the searched time window are considered; spans outside that window are not affected. The optional `start_time` and `end_time` fields scope the search to a specific time window. Each bound is independent: omitting `start_time` defaults to two years ago; omitting `end_time` defaults to now. You may provide either or both. Providing them when the approximate timestamp of the target spans is known significantly reduces the amount of span data the server must search. A `200 OK` response always includes: - `completed` — `true` if the operation finished and no retry is needed; `false` if the operation could not fully complete (retry the full request). - `deleted_span_ids` — span IDs confirmed deleted in this request. - `not_deleted_span_ids` — requested IDs not deleted: either not found within the supported time range, or not reached when `completed` is `false`. The delete operation is idempotent — re-submitting already-deleted IDs is safe. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param delete_spans_request: Body containing span IDs to delete (required) :type delete_spans_request: DeleteSpansRequest @@ -502,7 +502,7 @@ def delete_spans_without_preload_content( ) -> RESTResponseType: """Delete spans - Permanently deletes spans by their span IDs. This operation is irreversible. Accepts between 1 and 5000 span IDs per request. Only spans within the supported time range (2 years) are considered; older spans are not affected. A `200 OK` response always includes: - `completed` — `true` if the operation finished and no retry is needed; `false` if the operation could not fully complete (retry the full request). - `deleted_span_ids` — span IDs confirmed deleted in this request. - `not_deleted_span_ids` — requested IDs not deleted: either not found within the supported time range, or not reached when `completed` is `false`. The delete operation is idempotent — re-submitting already-deleted IDs is safe. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). + Permanently deletes spans by their span IDs. This operation is irreversible. Accepts between 1 and 5000 span IDs per request. Only spans within the searched time window are considered; spans outside that window are not affected. The optional `start_time` and `end_time` fields scope the search to a specific time window. Each bound is independent: omitting `start_time` defaults to two years ago; omitting `end_time` defaults to now. You may provide either or both. Providing them when the approximate timestamp of the target spans is known significantly reduces the amount of span data the server must search. A `200 OK` response always includes: - `completed` — `true` if the operation finished and no retry is needed; `false` if the operation could not fully complete (retry the full request). - `deleted_span_ids` — span IDs confirmed deleted in this request. - `not_deleted_span_ids` — requested IDs not deleted: either not found within the supported time range, or not reached when `completed` is `false`. The delete operation is idempotent — re-submitting already-deleted IDs is safe. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). :param delete_spans_request: Body containing span IDs to delete (required) :type delete_spans_request: DeleteSpansRequest diff --git a/src/arize/_generated/api_client/docs/APIKeysApi.md b/src/arize/_generated/api_client/docs/APIKeysApi.md index 7bd8442..a761246 100644 --- a/src/arize/_generated/api_client/docs/APIKeysApi.md +++ b/src/arize/_generated/api_client/docs/APIKeysApi.md @@ -29,7 +29,7 @@ Create a new API key for the authenticated user. organization roles default to `READ_ONLY`, and `account_role` defaults to `MEMBER`. **Authorization:** -- **User keys:** Requires the `developer` user permission flag. Returns `403` when this flag is absent. +- **User keys:** The authenticated user may create personal keys for themselves. - **Service keys:** Requires the `SERVICE_KEY_CREATE` permission in the target space (space member or above). @@ -140,9 +140,6 @@ caller with space access (not admin-gated). **User keys (`key_type=USER`):** Returned by default (no `space_id`). Provide `user_id` to view keys belonging to a specific user — account admins only; non-admins receive `403`. -**Authorization:** Requires the `developer` user permission flag or account admin role. -Returns `403` when neither condition is met. - This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). @@ -247,8 +244,7 @@ there is no window where neither key is valid. The full new key value (`key`) is **only returned once** in the response. Store it securely. **Authorization:** -- **User keys:** the creator or an account admin may refresh the key. Requires the - `developer` user permission flag. Returns `403` when this flag is absent. +- **User keys:** The creator or an account admin may refresh the key. - **Service keys:** space admins (and higher) may refresh any service key in their space. Non-admins require the `SERVICE_KEY_CREATE` permission and must be the creator of the key. @@ -353,11 +349,11 @@ Revoke an API key by its ID. The key will immediately stop working for authentic already-revoked key is a no-op and still returns `204`. **Authorization:** -Requires the `developer` user permission flag **or** account admin role (either condition is sufficient). -Returns `403` when neither condition is met. - -For service keys, only the key's creator or an account admin may revoke the key. -A developer who did not create the key receives `404` (prevents key-ID enumeration). +- **User keys:** The key's creator or an account admin may revoke the key. +- **Service keys:** Account admins and space admins in all of the key's bound spaces may + revoke the key regardless of who created it. All other callers must have the + `SERVICE_KEY_REVOKE` permission in every bound space and must be the key's creator. + Callers without read access to the key receive `404` to prevent key-ID enumeration. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). diff --git a/src/arize/_generated/api_client/docs/AddAnnotationQueueRecordsRequest.md b/src/arize/_generated/api_client/docs/AddAnnotationQueueRecordsRequest.md index 4dda849..ca2861e 100644 --- a/src/arize/_generated/api_client/docs/AddAnnotationQueueRecordsRequest.md +++ b/src/arize/_generated/api_client/docs/AddAnnotationQueueRecordsRequest.md @@ -5,7 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**record_sources** | [**List[AnnotationQueueRecordInput]**](AnnotationQueueRecordInput.md) | Record sources to add to the annotation queue. At most 2 record sources (projects or datasets) may be provided in a single request. | +**record_sources** | [**List[AnnotationQueueRecordInput]**](AnnotationQueueRecordInput.md) | Record sources to add to the annotation queue. At most 2 record sources (projects or datasets) may be provided in a single request. The total number of records resolved from all sources must not exceed 500. | ## Example diff --git a/src/arize/_generated/api_client/docs/AnnotationConfigsApi.md b/src/arize/_generated/api_client/docs/AnnotationConfigsApi.md index 9811ed2..359561f 100644 --- a/src/arize/_generated/api_client/docs/AnnotationConfigsApi.md +++ b/src/arize/_generated/api_client/docs/AnnotationConfigsApi.md @@ -128,7 +128,9 @@ Name | Type | Description | Notes Delete an annotation config -Delete an annotation config by its ID. This operation is irreversible. +Delete an annotation config by its ID. The annotation config must not be associated +with an active annotation queue; remove it from those queues before deleting it. +This operation is irreversible. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). @@ -202,6 +204,7 @@ void (empty response body) **401** | Authentication is required | - | **403** | Insufficient permissions to access this resource | - | **404** | Not found | - | +**409** | Resource conflict | - | **429** | Rate limit exceeded | * Retry-After - When throttled (429), how long to wait before retrying. Value is either a delta-seconds integer.
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/src/arize/_generated/api_client/docs/AnnotationQueueExampleRecordInput.md b/src/arize/_generated/api_client/docs/AnnotationQueueExampleRecordInput.md index 5fc7801..18cdabb 100644 --- a/src/arize/_generated/api_client/docs/AnnotationQueueExampleRecordInput.md +++ b/src/arize/_generated/api_client/docs/AnnotationQueueExampleRecordInput.md @@ -8,7 +8,7 @@ Name | Type | Description | Notes **record_type** | **str** | Discriminator identifying this record source as dataset examples. Must be `EXAMPLE` for dataset example records. | **dataset_id** | **str** | The dataset ID these examples belong to | **dataset_version_id** | **str** | Optional. The specific dataset version to use. If omitted, the latest version is used. | [optional] -**example_ids** | **List[str]** | Optional. List of example IDs within the dataset to add to the queue. If omitted, all examples in the dataset (or dataset version) are added. | [optional] +**example_ids** | **List[str]** | Optional. List of example IDs within the dataset to add to the queue. If omitted, all examples in the dataset (or dataset version) are added, provided the total records from all sources does not exceed 500. | [optional] ## Example diff --git a/src/arize/_generated/api_client/docs/AnnotationQueueRecordInput.md b/src/arize/_generated/api_client/docs/AnnotationQueueRecordInput.md index 0106e56..0841b7e 100644 --- a/src/arize/_generated/api_client/docs/AnnotationQueueRecordInput.md +++ b/src/arize/_generated/api_client/docs/AnnotationQueueRecordInput.md @@ -8,7 +8,7 @@ Name | Type | Description | Notes **record_type** | **str** | Discriminator identifying this record as a trace record. | **dataset_id** | **str** | The dataset ID these examples belong to | **dataset_version_id** | **str** | Optional. The specific dataset version to use. If omitted, the latest version is used. | [optional] -**example_ids** | **List[str]** | Optional. List of example IDs within the dataset to add to the queue. If omitted, all examples in the dataset (or dataset version) are added. | [optional] +**example_ids** | **List[str]** | Optional. List of example IDs within the dataset to add to the queue. If omitted, all examples in the dataset (or dataset version) are added, provided the total records from all sources does not exceed 500. | [optional] **project_id** | **str** | The project ID these traces belong to. | **start_time** | **datetime** | Start of the time range used to resolve each trace's root span. The range (end_time - start_time) must not exceed 7 days. | **end_time** | **datetime** | End of the time range. Must be after start_time. | diff --git a/src/arize/_generated/api_client/docs/AnnotationQueuesApi.md b/src/arize/_generated/api_client/docs/AnnotationQueuesApi.md index 7a145ea..4525797 100644 --- a/src/arize/_generated/api_client/docs/AnnotationQueuesApi.md +++ b/src/arize/_generated/api_client/docs/AnnotationQueuesApi.md @@ -457,7 +457,7 @@ Add new records from spans, traces, or dataset examples to an existing annotatio } ``` -If no example_ids are provided for a dataset record source, all examples in the dataset will be added to the queue. +If no example_ids are provided for a dataset record source, all examples in the dataset will be added to the queue only when the total records from all sources does not exceed 500. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). diff --git a/src/arize/_generated/api_client/docs/CreateAnnotationQueueRequest.md b/src/arize/_generated/api_client/docs/CreateAnnotationQueueRequest.md index 3345997..1de82c3 100644 --- a/src/arize/_generated/api_client/docs/CreateAnnotationQueueRequest.md +++ b/src/arize/_generated/api_client/docs/CreateAnnotationQueueRequest.md @@ -11,7 +11,7 @@ Name | Type | Description | Notes **annotation_config_ids** | **List[str]** | IDs of annotation configs to associate with this queue. All configs must belong to the same space. | **annotator_emails** | **List[str]** | Email addresses of annotators to assign to the queue. Emails are resolved to user IDs server-side. | **assignment_method** | [**AssignmentMethod**](AssignmentMethod.md) | How records are assigned to annotators. Defaults to `ALL` when omitted. | [optional] -**record_sources** | [**List[AnnotationQueueRecordInput]**](AnnotationQueueRecordInput.md) | Record sources to add to the annotation queue on creation. At most 2 record sources (projects or datasets) may be provided in a single create request. Additional records from other sources can be added after creation. | [optional] +**record_sources** | [**List[AnnotationQueueRecordInput]**](AnnotationQueueRecordInput.md) | Record sources to add to the annotation queue on creation. At most 2 record sources (projects or datasets) may be provided in a single create request. The total number of records resolved from all sources must not exceed 500. Additional records from other sources can be added after creation. | [optional] ## Example diff --git a/src/arize/_generated/api_client/docs/CreateExperimentRequest.md b/src/arize/_generated/api_client/docs/CreateExperimentRequest.md index cc51f1a..5a3e237 100644 --- a/src/arize/_generated/api_client/docs/CreateExperimentRequest.md +++ b/src/arize/_generated/api_client/docs/CreateExperimentRequest.md @@ -1,13 +1,14 @@ # CreateExperimentRequest -Experiment creation parameters with an initial set of runs. +Experiment creation parameters with an initial set of runs. An experiment belongs to a space and may optionally be associated with a dataset. Provide exactly one of: - `dataset_id` — associate the experiment with a dataset; it's created in that dataset's space, and its runs may reference the dataset's examples via `example_id`. - `space_id` — the space to create the experiment in, when it isn't associated with a dataset. Providing both, or neither, is a validation error. ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **name** | **str** | Name of the experiment | -**dataset_id** | **str** | ID of the dataset to create the experiment for | +**dataset_id** | **str** | ID of the dataset to associate the experiment with. Provide `space_id` instead when the experiment isn't associated with a dataset. | [optional] +**space_id** | **str** | ID of the space to create the experiment in. Provide instead of `dataset_id`. | [optional] **experiment_runs** | [**List[ExperimentRunInput]**](ExperimentRunInput.md) | Array of experiment run data | ## Example diff --git a/src/arize/_generated/api_client/docs/DeleteSpansRequest.md b/src/arize/_generated/api_client/docs/DeleteSpansRequest.md index 02145a5..96d1f73 100644 --- a/src/arize/_generated/api_client/docs/DeleteSpansRequest.md +++ b/src/arize/_generated/api_client/docs/DeleteSpansRequest.md @@ -7,6 +7,8 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **project_id** | **str** | The project ID containing the spans to delete | **span_ids** | **List[str]** | List of span IDs to delete (maximum 5000) | +**start_time** | **datetime** | Scope the delete to spans starting at or after this timestamp (inclusive). ISO 8601 format (e.g., `2024-01-01T00:00:00Z`). Each bound is independent: omitting `start_time` defaults to two years ago; omitting `end_time` defaults to now. You may provide either or both. | [optional] +**end_time** | **datetime** | Scope the delete to spans starting before this timestamp (exclusive). ISO 8601 format (e.g., `2024-01-02T00:00:00Z`). Each bound is independent: omitting `start_time` defaults to two years ago; omitting `end_time` defaults to now. You may provide either or both. | [optional] ## Example diff --git a/src/arize/_generated/api_client/docs/Experiment.md b/src/arize/_generated/api_client/docs/Experiment.md index f7258c8..8eda52f 100644 --- a/src/arize/_generated/api_client/docs/Experiment.md +++ b/src/arize/_generated/api_client/docs/Experiment.md @@ -8,6 +8,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **id** | **str** | Unique identifier for the experiment | **name** | **str** | Name of the experiment | +**space_id** | **str** | Unique identifier for the space this experiment belongs to | **dataset_id** | **str** | Unique identifier for the dataset associated with this experiment. Null if the experiment isn't associated with a dataset. | [optional] **dataset_version_id** | **str** | Unique identifier for the dataset version associated with this experiment. Null if the experiment isn't associated with a dataset. | [optional] **created_at** | **datetime** | Timestamp for when the experiment was created | diff --git a/src/arize/_generated/api_client/docs/ExperimentRun.md b/src/arize/_generated/api_client/docs/ExperimentRun.md index 52f9462..2fe6a32 100644 --- a/src/arize/_generated/api_client/docs/ExperimentRun.md +++ b/src/arize/_generated/api_client/docs/ExperimentRun.md @@ -7,7 +7,7 @@ An experiment run with experiment data including outputs, evaluations, and trace Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **id** | **str** | System-assigned unique ID for the example | [readonly] -**example_id** | **str** | ID of the dataset example associated with this experiment run | [readonly] +**example_id** | **str** | ID of the dataset example associated with this experiment run. Null when the experiment isn't associated with a dataset. | [optional] [readonly] **output** | **str** | Output of the task for the matching example. Null when the task errored. | [optional] **error** | **str** | Error message when the task failed. Null on success. | [optional] **annotations** | [**List[Annotation]**](Annotation.md) | List of human annotations on this experiment run | [optional] [readonly] diff --git a/src/arize/_generated/api_client/docs/ExperimentRunInput.md b/src/arize/_generated/api_client/docs/ExperimentRunInput.md index 9f89740..8b4202b 100644 --- a/src/arize/_generated/api_client/docs/ExperimentRunInput.md +++ b/src/arize/_generated/api_client/docs/ExperimentRunInput.md @@ -6,7 +6,7 @@ An experiment run with experiment data including outputs, evaluations, and trace Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**example_id** | **str** | ID of the dataset example associated with this experiment run | +**example_id** | **str** | ID of the dataset example associated with this experiment run. Provided when the experiment is associated with a dataset; omitted otherwise. | [optional] **output** | **str** | output of the task for the matching example | ## Example diff --git a/src/arize/_generated/api_client/docs/ExperimentWithRunIds.md b/src/arize/_generated/api_client/docs/ExperimentWithRunIds.md index 28b92be..b9a9bbd 100644 --- a/src/arize/_generated/api_client/docs/ExperimentWithRunIds.md +++ b/src/arize/_generated/api_client/docs/ExperimentWithRunIds.md @@ -7,6 +7,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **id** | **str** | Unique identifier for the experiment | **name** | **str** | Name of the experiment | +**space_id** | **str** | Unique identifier for the space this experiment belongs to | **dataset_id** | **str** | Unique identifier for the dataset associated with this experiment. Null if the experiment isn't associated with a dataset. | [optional] **dataset_version_id** | **str** | Unique identifier for the dataset version associated with this experiment. Null if the experiment isn't associated with a dataset. | [optional] **created_at** | **datetime** | Timestamp for when the experiment was created | diff --git a/src/arize/_generated/api_client/docs/ExperimentsApi.md b/src/arize/_generated/api_client/docs/ExperimentsApi.md index b6121c4..f2b468a 100644 --- a/src/arize/_generated/api_client/docs/ExperimentsApi.md +++ b/src/arize/_generated/api_client/docs/ExperimentsApi.md @@ -143,19 +143,25 @@ Create an experiment Create a new experiment. Empty experiments are not allowed. +An experiment belongs to a space and may optionally be associated with a +dataset. + Experiments are composed of "runs". Each experiment run (JSON object) -must include an `example_id` field that corresponds to an example in -the dataset, and a `output` field that contains the task's output for -the example (the input). +must include an `output` field containing the task's output. When the +experiment is associated with a dataset, each run must also include an +`example_id` referencing an example in that dataset. Payload Requirements -- The `name` must be unique within the target dataset +- Provide exactly one of `dataset_id` or `space_id`. +- The `name` must be unique within the dataset it's associated with, or + within the space when it isn't associated with a dataset. - Provide at least one run in `experiment_runs`. - Each run must include: - - `example_id` -- the ID of an existing example in the dataset/version - - `output` -- model/task output for that example + - `output` -- model/task output for the run + - `example_id` -- the ID of an existing example in the dataset, + required only when the experiment is associated with a dataset - You may include any additional fields per run that can be used for - analysis or filtering. For exampple: `model`, `latency_ms`, + analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). @@ -421,8 +427,9 @@ Append new runs to an existing experiment. **Payload Requirements** - Provide between 1 and 1000 runs in `experiment_runs`. - Each run must include: - - `example_id` -- the ID of an existing example in the dataset version - - `output` -- model/task output for that example + - `output` -- model/task output for the run + - `example_id` -- the ID of an existing example in the dataset, + required only when the experiment is associated with a dataset - You may include any additional fields per run that can be used for analysis or filtering. For example: `model`, `latency_ms`, `temperature`, `prompt`, `tool_calls`, etc. @@ -631,14 +638,20 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **list_experiments** -> ListExperimentsResponse list_experiments(dataset_id=dataset_id, name=name, limit=limit, cursor=cursor) +> ListExperimentsResponse list_experiments(dataset_id=dataset_id, space_id=space_id, name=name, limit=limit, cursor=cursor) List experiments -List all experiments a user has access to. +List experiments a user has access to. + +By default, lists every accessible experiment across all spaces the caller +can read, including experiments that are not associated with a dataset. + +To narrow the results, provide at most one of: +- `dataset_id` — only experiments run on that dataset. +- `space_id` — only experiments in that space (with or without a dataset). -To filter experiments by the dataset they were run on, provide the -`dataset_id` query parameter. +Providing both `dataset_id` and `space_id` is a validation error. This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). @@ -674,13 +687,14 @@ with arize._generated.api_client.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = arize._generated.api_client.ExperimentsApi(api_client) dataset_id = 'RGF0YXNldDoxMjM0NQ==' # str | Filter to a specific dataset (base64 identifier (base64)) (optional) + space_id = 'U3BhY2U6MTIzNDU=' # str | Filter search results to a particular space ID (optional) name = 'production' # str | Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. (optional) limit = 50 # int | Maximum items to return (optional) (default to 50) cursor = 'cursor_example' # str | Opaque pagination cursor returned from a previous response (`pagination.next_cursor`). Treat it as an unreadable token; do not attempt to parse or construct it. (optional) try: # List experiments - api_response = api_instance.list_experiments(dataset_id=dataset_id, name=name, limit=limit, cursor=cursor) + api_response = api_instance.list_experiments(dataset_id=dataset_id, space_id=space_id, name=name, limit=limit, cursor=cursor) print("The response of ExperimentsApi->list_experiments:\n") pprint(api_response) except Exception as e: @@ -695,6 +709,7 @@ with arize._generated.api_client.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **dataset_id** | **str**| Filter to a specific dataset (base64 identifier (base64)) | [optional] + **space_id** | **str**| Filter search results to a particular space ID | [optional] **name** | **str**| Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. | [optional] **limit** | **int**| Maximum items to return | [optional] [default to 50] **cursor** | **str**| Opaque pagination cursor returned from a previous response (`pagination.next_cursor`). Treat it as an unreadable token; do not attempt to parse or construct it. | [optional] diff --git a/src/arize/_generated/api_client/docs/IntegrationsApi.md b/src/arize/_generated/api_client/docs/IntegrationsApi.md index 8b9ab5c..c17c6bf 100644 --- a/src/arize/_generated/api_client/docs/IntegrationsApi.md +++ b/src/arize/_generated/api_client/docs/IntegrationsApi.md @@ -271,18 +271,23 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **list_integrations** -> ListIntegrationsResponse list_integrations(type, space_id=space_id, space_name=space_name, name=name, limit=limit, cursor=cursor) +> ListIntegrationsResponse list_integrations(type=type, space_id=space_id, space_name=space_name, name=name, limit=limit, cursor=cursor) List integrations -List integrations the user has access to. `type` is required and the -response contains only integrations of that type. Each item still -carries its `type` (and, for `LLM`, `config.provider`) for client-side -discrimination. A missing or invalid `type` returns `400`. +List integrations the user has access to, ordered by creation time +(newest first). By default the list includes every integration type; +pass `type` to list a single type. Each item carries its `type` (and, +for `LLM`, `config.provider`) for client-side discrimination. An +invalid `type` or pagination `cursor` returns `400`; a cursor is only +valid for the query parameters it was issued with. Integrations are owned at the account level but carry visibility scopings (account-wide, organization, or space). `space_id` / `space_name` filter -the list to integrations visible in a given space. +the list to integrations visible in a given space. The list contains +only the types the caller has permission to read. When no type is +readable the request fails with `403` — or `404` when a `space_id` +filter references a space outside the caller's visibility. This endpoint is in alpha, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages). @@ -318,7 +323,7 @@ configuration = arize._generated.api_client.Configuration( with arize._generated.api_client.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = arize._generated.api_client.IntegrationsApi(api_client) - type = arize._generated.api_client.IntegrationType() # IntegrationType | The integration type to list. Required - the list returns only integrations of this type. + type = arize._generated.api_client.IntegrationType() # IntegrationType | Filter the list to a single integration type. When omitted, integrations of every type are returned; each item carries its `type` for client-side discrimination. (optional) space_id = 'U3BhY2U6MTIzNDU=' # str | Filter search results to a particular space ID (optional) space_name = 'my-space' # str | Case-insensitive substring filter on the space name. Narrows results to resources in spaces whose name contains the given string. If omitted, no space name filtering is applied and all resources are returned. (optional) name = 'production' # str | Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. (optional) @@ -327,7 +332,7 @@ with arize._generated.api_client.ApiClient(configuration) as api_client: try: # List integrations - api_response = api_instance.list_integrations(type, space_id=space_id, space_name=space_name, name=name, limit=limit, cursor=cursor) + api_response = api_instance.list_integrations(type=type, space_id=space_id, space_name=space_name, name=name, limit=limit, cursor=cursor) print("The response of IntegrationsApi->list_integrations:\n") pprint(api_response) except Exception as e: @@ -341,7 +346,7 @@ with arize._generated.api_client.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **type** | [**IntegrationType**](.md)| The integration type to list. Required - the list returns only integrations of this type. | + **type** | [**IntegrationType**](.md)| Filter the list to a single integration type. When omitted, integrations of every type are returned; each item carries its `type` for client-side discrimination. | [optional] **space_id** | **str**| Filter search results to a particular space ID | [optional] **space_name** | **str**| Case-insensitive substring filter on the space name. Narrows results to resources in spaces whose name contains the given string. If omitted, no space name filtering is applied and all resources are returned. | [optional] **name** | **str**| Case-insensitive substring filter on the resource name. Returns only resources whose name contains the given string. For example, `name=prod` matches \"production\", \"my-prod-dataset\", etc. If omitted, no name filtering is applied and all resources are returned. | [optional] diff --git a/src/arize/_generated/api_client/docs/SpansApi.md b/src/arize/_generated/api_client/docs/SpansApi.md index 3e61b31..494c973 100644 --- a/src/arize/_generated/api_client/docs/SpansApi.md +++ b/src/arize/_generated/api_client/docs/SpansApi.md @@ -164,7 +164,14 @@ Delete spans Permanently deletes spans by their span IDs. This operation is irreversible. Accepts between 1 and 5000 span IDs per request. Only spans within the -supported time range (2 years) are considered; older spans are not affected. +searched time window are considered; spans outside that window are not affected. + +The optional `start_time` and `end_time` fields scope the search to a +specific time window. Each bound is independent: omitting `start_time` +defaults to two years ago; omitting `end_time` defaults to now. You may +provide either or both. Providing them when the approximate timestamp of +the target spans is known significantly reduces the amount of span data +the server must search. A `200 OK` response always includes: - `completed` — `true` if the operation finished and no retry is needed; diff --git a/src/arize/_generated/api_client/docs/UpdateAnnotationQueueRequest.md b/src/arize/_generated/api_client/docs/UpdateAnnotationQueueRequest.md index c1d8921..c679887 100644 --- a/src/arize/_generated/api_client/docs/UpdateAnnotationQueueRequest.md +++ b/src/arize/_generated/api_client/docs/UpdateAnnotationQueueRequest.md @@ -6,7 +6,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **name** | **str** | The name of the annotation queue. Must be unique within the space. | [optional] -**instructions** | **str** | The instructions for annotators working on this queue. Send an empty string to clear the instructions. | [optional] +**instructions** | **str** | The instructions for annotators working on this queue. Set to `null` to clear the instructions. | [optional] **annotation_config_ids** | **List[str]** | The full list of annotation config IDs to associate with this queue. This replaces all existing annotation config associations. All annotation configs must belong to the same space as the queue. | [optional] **annotator_emails** | **List[str]** | The full list of user emails to assign to this queue. This replaces all existing user assignments. All users must have an active account and access to the queue's space. | [optional] diff --git a/src/arize/_generated/api_client/docs/UpdateLlmConfig.md b/src/arize/_generated/api_client/docs/UpdateLlmConfig.md index 755a1fb..1f88df7 100644 --- a/src/arize/_generated/api_client/docs/UpdateLlmConfig.md +++ b/src/arize/_generated/api_client/docs/UpdateLlmConfig.md @@ -7,8 +7,8 @@ Partial LLM config for PATCH. `provider` is immutable; if present it must match Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **provider** | [**LlmIntegrationProvider**](LlmIntegrationProvider.md) | | [optional] -**api_key** | **str** | Rotate the API key. Pass null to clear it. Omit to keep unchanged. Not valid for `AWS_BEDROCK` (bearer tokens are rotated via `auth`). | [optional] -**is_function_calling_enabled** | **bool** | Enable or disable function/tool calling. Omit to keep unchanged. Not valid for `AWS_BEDROCK`. | [optional] +**api_key** | **str** | Rotate the API key. Pass null to clear it. Omit to keep unchanged. Not valid for `AWS_BEDROCK` (bearer tokens are rotated via `auth`) or `VERTEX_AI`. | [optional] +**is_function_calling_enabled** | **bool** | Enable or disable function/tool calling. Omit to keep unchanged. Not valid for `AWS_BEDROCK` or `VERTEX_AI`. | [optional] **auth** | [**CreateAwsBedrockAuth**](CreateAwsBedrockAuth.md) | | [optional] **base_url** | **str** | (`CUSTOM` and `NVIDIA_NIM` only) New endpoint URL. For `NVIDIA_NIM` the field is optional on the resource, so null clears it (falling back to the provider default endpoint). For `CUSTOM` it is required on the resource — null is rejected with 422. Omit to keep unchanged. | [optional] **headers** | **Dict[str, str]** | (`CUSTOM` and `NVIDIA_NIM` only) Replaces the configured custom request headers: the provided map becomes the full header set. Pass null to clear all headers. Omit to keep unchanged. Write-only; names are exposed as `header_names` on read. The serialized header map must not exceed 8,175 bytes. | [optional] diff --git a/src/arize/_generated/api_client/docs/UpdateOrganizationRequest.md b/src/arize/_generated/api_client/docs/UpdateOrganizationRequest.md index 66ac21c..e123fff 100644 --- a/src/arize/_generated/api_client/docs/UpdateOrganizationRequest.md +++ b/src/arize/_generated/api_client/docs/UpdateOrganizationRequest.md @@ -6,7 +6,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **name** | **str** | Updated name for the organization (must be unique within the account) | [optional] -**description** | **str** | Updated description for the organization. Set to an empty string to clear it. | [optional] +**description** | **str** | Updated description for the organization. Set to `null` to clear it. | [optional] ## Example diff --git a/src/arize/_generated/api_client/docs/UpdateRoleRequest.md b/src/arize/_generated/api_client/docs/UpdateRoleRequest.md index 08b938d..701b1be 100644 --- a/src/arize/_generated/api_client/docs/UpdateRoleRequest.md +++ b/src/arize/_generated/api_client/docs/UpdateRoleRequest.md @@ -6,7 +6,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **name** | **str** | Updated name for the role. Must be unique within the account. | [optional] -**description** | **str** | Updated description of the role. | [optional] +**description** | **str** | Updated description of the role. Set to `null` to clear it. | [optional] **permissions** | [**List[Permission]**](Permission.md) | Replacement set of permissions. When provided, the existing permissions are fully replaced. Each value must be a valid permission identifier. | [optional] ## Example diff --git a/src/arize/_generated/api_client/docs/UpdateSpaceRequest.md b/src/arize/_generated/api_client/docs/UpdateSpaceRequest.md index dfd9ef9..dca8ca0 100644 --- a/src/arize/_generated/api_client/docs/UpdateSpaceRequest.md +++ b/src/arize/_generated/api_client/docs/UpdateSpaceRequest.md @@ -6,7 +6,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **name** | **str** | Updated name of the space | [optional] -**description** | **str** | Updated description of the space | [optional] +**description** | **str** | Updated description of the space. Set to `null` to clear it. | [optional] **is_private** | **bool** | Updated visibility for the space. Set to `true` to make the space private (visible only to members and admins), or `false` to make it public. When omitted, the existing visibility is preserved. | [optional] ## Example diff --git a/src/arize/_generated/api_client/models/add_annotation_queue_records_request.py b/src/arize/_generated/api_client/models/add_annotation_queue_records_request.py index ead9f3c..61e6701 100644 --- a/src/arize/_generated/api_client/models/add_annotation_queue_records_request.py +++ b/src/arize/_generated/api_client/models/add_annotation_queue_records_request.py @@ -28,7 +28,7 @@ class AddAnnotationQueueRecordsRequest(BaseModel): """ AddAnnotationQueueRecordsRequest """ # noqa: E501 - record_sources: Annotated[List[AnnotationQueueRecordInput], Field(min_length=1, max_length=2)] = Field(description="Record sources to add to the annotation queue. At most 2 record sources (projects or datasets) may be provided in a single request.") + record_sources: Annotated[List[AnnotationQueueRecordInput], Field(min_length=1, max_length=2)] = Field(description="Record sources to add to the annotation queue. At most 2 record sources (projects or datasets) may be provided in a single request. The total number of records resolved from all sources must not exceed 500.") __properties: ClassVar[List[str]] = ["record_sources"] model_config = ConfigDict( diff --git a/src/arize/_generated/api_client/models/annotate_annotation_queue_record_response.py b/src/arize/_generated/api_client/models/annotate_annotation_queue_record_response.py index 456c2e4..446b2d1 100644 --- a/src/arize/_generated/api_client/models/annotate_annotation_queue_record_response.py +++ b/src/arize/_generated/api_client/models/annotate_annotation_queue_record_response.py @@ -98,10 +98,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in AnnotateAnnotationQueueRecordResponse) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/annotation_queue.py b/src/arize/_generated/api_client/models/annotation_queue.py index 6622453..14b4f92 100644 --- a/src/arize/_generated/api_client/models/annotation_queue.py +++ b/src/arize/_generated/api_client/models/annotation_queue.py @@ -108,10 +108,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in AnnotationQueue) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/annotation_queue_assigned_user.py b/src/arize/_generated/api_client/models/annotation_queue_assigned_user.py index f84bbe7..cba48c0 100644 --- a/src/arize/_generated/api_client/models/annotation_queue_assigned_user.py +++ b/src/arize/_generated/api_client/models/annotation_queue_assigned_user.py @@ -85,10 +85,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in AnnotationQueueAssignedUser) in the input: " + _key) _obj = cls.model_validate({ "user": AnnotatorUser.from_dict(obj["user"]) if obj.get("user") is not None else None, diff --git a/src/arize/_generated/api_client/models/annotation_queue_example_record_input.py b/src/arize/_generated/api_client/models/annotation_queue_example_record_input.py index ae45135..10e25cd 100644 --- a/src/arize/_generated/api_client/models/annotation_queue_example_record_input.py +++ b/src/arize/_generated/api_client/models/annotation_queue_example_record_input.py @@ -19,6 +19,7 @@ from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated from typing import Optional, Set from typing_extensions import Self @@ -29,7 +30,7 @@ class AnnotationQueueExampleRecordInput(BaseModel): record_type: StrictStr = Field(description="Discriminator identifying this record source as dataset examples. Must be `EXAMPLE` for dataset example records.") dataset_id: StrictStr = Field(description="The dataset ID these examples belong to") dataset_version_id: Optional[StrictStr] = Field(default=None, description="Optional. The specific dataset version to use. If omitted, the latest version is used. ") - example_ids: Optional[List[StrictStr]] = Field(default=None, description="Optional. List of example IDs within the dataset to add to the queue. If omitted, all examples in the dataset (or dataset version) are added. ") + example_ids: Optional[Annotated[List[StrictStr], Field(max_length=500)]] = Field(default=None, description="Optional. List of example IDs within the dataset to add to the queue. If omitted, all examples in the dataset (or dataset version) are added, provided the total records from all sources does not exceed 500. ") __properties: ClassVar[List[str]] = ["record_type", "dataset_id", "dataset_version_id", "example_ids"] @field_validator('record_type') diff --git a/src/arize/_generated/api_client/models/annotation_queue_record.py b/src/arize/_generated/api_client/models/annotation_queue_record.py index 9b8b652..40ac5c5 100644 --- a/src/arize/_generated/api_client/models/annotation_queue_record.py +++ b/src/arize/_generated/api_client/models/annotation_queue_record.py @@ -125,10 +125,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in AnnotationQueueRecord) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/annotation_queue_span_record_input.py b/src/arize/_generated/api_client/models/annotation_queue_span_record_input.py index bc959eb..821e835 100644 --- a/src/arize/_generated/api_client/models/annotation_queue_span_record_input.py +++ b/src/arize/_generated/api_client/models/annotation_queue_span_record_input.py @@ -32,7 +32,7 @@ class AnnotationQueueSpanRecordInput(BaseModel): project_id: StrictStr = Field(description="The project ID these spans belong to") start_time: datetime = Field(description="Start of the time range to search for spans. The range (end_time - start_time) must not exceed 7 days. ") end_time: datetime = Field(description="End of the time range. Must be after start_time. ") - span_ids: Annotated[List[StrictStr], Field(min_length=1)] = Field(description="List of span IDs to add to the queue") + span_ids: Annotated[List[StrictStr], Field(min_length=1, max_length=500)] = Field(description="List of span IDs to add to the queue") __properties: ClassVar[List[str]] = ["record_type", "project_id", "start_time", "end_time", "span_ids"] @field_validator('record_type') diff --git a/src/arize/_generated/api_client/models/annotation_queue_trace_record_input.py b/src/arize/_generated/api_client/models/annotation_queue_trace_record_input.py index 2849d5d..b2d5b09 100644 --- a/src/arize/_generated/api_client/models/annotation_queue_trace_record_input.py +++ b/src/arize/_generated/api_client/models/annotation_queue_trace_record_input.py @@ -32,7 +32,7 @@ class AnnotationQueueTraceRecordInput(BaseModel): project_id: StrictStr = Field(description="The project ID these traces belong to.") start_time: datetime = Field(description="Start of the time range used to resolve each trace's root span. The range (end_time - start_time) must not exceed 7 days. ") end_time: datetime = Field(description="End of the time range. Must be after start_time. ") - trace_ids: Annotated[List[StrictStr], Field(min_length=1)] = Field(description="List of trace IDs to add to the queue. ") + trace_ids: Annotated[List[StrictStr], Field(min_length=1, max_length=500)] = Field(description="List of trace IDs to add to the queue. ") __properties: ClassVar[List[str]] = ["record_type", "project_id", "start_time", "end_time", "trace_ids"] @field_validator('record_type') diff --git a/src/arize/_generated/api_client/models/annotator_user.py b/src/arize/_generated/api_client/models/annotator_user.py index e2e5664..813ef7c 100644 --- a/src/arize/_generated/api_client/models/annotator_user.py +++ b/src/arize/_generated/api_client/models/annotator_user.py @@ -80,10 +80,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in AnnotatorUser) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/assign_annotation_queue_record_response.py b/src/arize/_generated/api_client/models/assign_annotation_queue_record_response.py index f76ffcd..ec667fb 100644 --- a/src/arize/_generated/api_client/models/assign_annotation_queue_record_response.py +++ b/src/arize/_generated/api_client/models/assign_annotation_queue_record_response.py @@ -98,10 +98,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in AssignAnnotationQueueRecordResponse) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/create_annotation_queue_record_response.py b/src/arize/_generated/api_client/models/create_annotation_queue_record_response.py index e9ebb1c..557bc33 100644 --- a/src/arize/_generated/api_client/models/create_annotation_queue_record_response.py +++ b/src/arize/_generated/api_client/models/create_annotation_queue_record_response.py @@ -87,10 +87,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in CreateAnnotationQueueRecordResponse) in the input: " + _key) _obj = cls.model_validate({ "record_sources": [AnnotationQueueRecord.from_dict(_item) for _item in obj["record_sources"]] if obj.get("record_sources") is not None else None diff --git a/src/arize/_generated/api_client/models/create_annotation_queue_request.py b/src/arize/_generated/api_client/models/create_annotation_queue_request.py index 9ba20f9..a50483e 100644 --- a/src/arize/_generated/api_client/models/create_annotation_queue_request.py +++ b/src/arize/_generated/api_client/models/create_annotation_queue_request.py @@ -35,7 +35,7 @@ class CreateAnnotationQueueRequest(BaseModel): annotation_config_ids: Annotated[List[StrictStr], Field(min_length=1)] = Field(description="IDs of annotation configs to associate with this queue. All configs must belong to the same space.") annotator_emails: Annotated[List[StrictStr], Field(min_length=1)] = Field(description="Email addresses of annotators to assign to the queue. Emails are resolved to user IDs server-side.") assignment_method: Optional[AssignmentMethod] = Field(default=None, description="How records are assigned to annotators. Defaults to `ALL` when omitted.") - record_sources: Optional[Annotated[List[AnnotationQueueRecordInput], Field(max_length=2)]] = Field(default=None, description="Record sources to add to the annotation queue on creation. At most 2 record sources (projects or datasets) may be provided in a single create request. Additional records from other sources can be added after creation.") + record_sources: Optional[Annotated[List[AnnotationQueueRecordInput], Field(max_length=2)]] = Field(default=None, description="Record sources to add to the annotation queue on creation. At most 2 record sources (projects or datasets) may be provided in a single create request. The total number of records resolved from all sources must not exceed 500. Additional records from other sources can be added after creation.") __properties: ClassVar[List[str]] = ["name", "space_id", "instructions", "annotation_config_ids", "annotator_emails", "assignment_method", "record_sources"] model_config = ConfigDict( diff --git a/src/arize/_generated/api_client/models/create_experiment_request.py b/src/arize/_generated/api_client/models/create_experiment_request.py index 06ceeb8..b9249b2 100644 --- a/src/arize/_generated/api_client/models/create_experiment_request.py +++ b/src/arize/_generated/api_client/models/create_experiment_request.py @@ -18,19 +18,20 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional from arize._generated.api_client.models.experiment_run_input import ExperimentRunInput from typing import Optional, Set from typing_extensions import Self class CreateExperimentRequest(BaseModel): """ - Experiment creation parameters with an initial set of runs. + Experiment creation parameters with an initial set of runs. An experiment belongs to a space and may optionally be associated with a dataset. Provide exactly one of: - `dataset_id` — associate the experiment with a dataset; it's created in that dataset's space, and its runs may reference the dataset's examples via `example_id`. - `space_id` — the space to create the experiment in, when it isn't associated with a dataset. Providing both, or neither, is a validation error. """ # noqa: E501 name: StrictStr = Field(description="Name of the experiment") - dataset_id: StrictStr = Field(description="ID of the dataset to create the experiment for") + dataset_id: Optional[StrictStr] = Field(default=None, description="ID of the dataset to associate the experiment with. Provide `space_id` instead when the experiment isn't associated with a dataset.") + space_id: Optional[StrictStr] = Field(default=None, description="ID of the space to create the experiment in. Provide instead of `dataset_id`.") experiment_runs: List[ExperimentRunInput] = Field(description="Array of experiment run data") - __properties: ClassVar[List[str]] = ["name", "dataset_id", "experiment_runs"] + __properties: ClassVar[List[str]] = ["name", "dataset_id", "space_id", "experiment_runs"] model_config = ConfigDict( populate_by_name=True, @@ -78,6 +79,16 @@ def to_dict(self) -> Dict[str, Any]: if _item_experiment_runs: _items.append(_item_experiment_runs.to_dict()) _dict['experiment_runs'] = _items + # set to None if dataset_id (nullable) is None + # and model_fields_set contains the field + if self.dataset_id is None and "dataset_id" in self.model_fields_set: + _dict['dataset_id'] = None + + # set to None if space_id (nullable) is None + # and model_fields_set contains the field + if self.space_id is None and "space_id" in self.model_fields_set: + _dict['space_id'] = None + return _dict @classmethod @@ -97,6 +108,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "name": obj.get("name"), "dataset_id": obj.get("dataset_id"), + "space_id": obj.get("space_id"), "experiment_runs": [ExperimentRunInput.from_dict(_item) for _item in obj["experiment_runs"]] if obj.get("experiment_runs") is not None else None }) return _obj diff --git a/src/arize/_generated/api_client/models/custom_baseline_config.py b/src/arize/_generated/api_client/models/custom_baseline_config.py index 89b3ffe..22c28a4 100644 --- a/src/arize/_generated/api_client/models/custom_baseline_config.py +++ b/src/arize/_generated/api_client/models/custom_baseline_config.py @@ -101,10 +101,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in CustomBaselineConfig) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/custom_metric_monitor.py b/src/arize/_generated/api_client/models/custom_metric_monitor.py index a8fab1f..d6d5cae 100644 --- a/src/arize/_generated/api_client/models/custom_metric_monitor.py +++ b/src/arize/_generated/api_client/models/custom_metric_monitor.py @@ -148,10 +148,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in CustomMetricMonitor) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/data_quality_monitor.py b/src/arize/_generated/api_client/models/data_quality_monitor.py index dfa2c81..9b9f202 100644 --- a/src/arize/_generated/api_client/models/data_quality_monitor.py +++ b/src/arize/_generated/api_client/models/data_quality_monitor.py @@ -159,10 +159,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DataQualityMonitor) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/dataset.py b/src/arize/_generated/api_client/models/dataset.py index 765158c..218756d 100644 --- a/src/arize/_generated/api_client/models/dataset.py +++ b/src/arize/_generated/api_client/models/dataset.py @@ -93,10 +93,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in Dataset) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/dataset_version.py b/src/arize/_generated/api_client/models/dataset_version.py index a189948..3a15852 100644 --- a/src/arize/_generated/api_client/models/dataset_version.py +++ b/src/arize/_generated/api_client/models/dataset_version.py @@ -84,10 +84,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DatasetVersion) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/dataset_version_with_example_ids.py b/src/arize/_generated/api_client/models/dataset_version_with_example_ids.py index 68caf29..5c1e339 100644 --- a/src/arize/_generated/api_client/models/dataset_version_with_example_ids.py +++ b/src/arize/_generated/api_client/models/dataset_version_with_example_ids.py @@ -86,10 +86,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DatasetVersionWithExampleIds) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/delete_dataset_examples_problem.py b/src/arize/_generated/api_client/models/delete_dataset_examples_problem.py index 0718c2f..1dc3aed 100644 --- a/src/arize/_generated/api_client/models/delete_dataset_examples_problem.py +++ b/src/arize/_generated/api_client/models/delete_dataset_examples_problem.py @@ -85,10 +85,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DeleteDatasetExamplesProblem) in the input: " + _key) _obj = cls.model_validate({ "title": obj.get("title"), diff --git a/src/arize/_generated/api_client/models/delete_dataset_examples_response.py b/src/arize/_generated/api_client/models/delete_dataset_examples_response.py index a437372..2fe4f9b 100644 --- a/src/arize/_generated/api_client/models/delete_dataset_examples_response.py +++ b/src/arize/_generated/api_client/models/delete_dataset_examples_response.py @@ -81,10 +81,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DeleteDatasetExamplesResponse) in the input: " + _key) _obj = cls.model_validate({ "completed": obj.get("completed"), diff --git a/src/arize/_generated/api_client/models/delete_spans_problem.py b/src/arize/_generated/api_client/models/delete_spans_problem.py index 4e10ef1..1ca9959 100644 --- a/src/arize/_generated/api_client/models/delete_spans_problem.py +++ b/src/arize/_generated/api_client/models/delete_spans_problem.py @@ -85,10 +85,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DeleteSpansProblem) in the input: " + _key) _obj = cls.model_validate({ "title": obj.get("title"), diff --git a/src/arize/_generated/api_client/models/delete_spans_request.py b/src/arize/_generated/api_client/models/delete_spans_request.py index 3a207b5..7498ba7 100644 --- a/src/arize/_generated/api_client/models/delete_spans_request.py +++ b/src/arize/_generated/api_client/models/delete_spans_request.py @@ -17,8 +17,9 @@ import re # noqa: F401 import json +from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional from typing_extensions import Annotated from typing import Optional, Set from typing_extensions import Self @@ -29,7 +30,9 @@ class DeleteSpansRequest(BaseModel): """ # noqa: E501 project_id: StrictStr = Field(description="The project ID containing the spans to delete") span_ids: Annotated[List[StrictStr], Field(min_length=1, max_length=5000)] = Field(description="List of span IDs to delete (maximum 5000)") - __properties: ClassVar[List[str]] = ["project_id", "span_ids"] + start_time: Optional[datetime] = Field(default=None, description="Scope the delete to spans starting at or after this timestamp (inclusive). ISO 8601 format (e.g., `2024-01-01T00:00:00Z`). Each bound is independent: omitting `start_time` defaults to two years ago; omitting `end_time` defaults to now. You may provide either or both. ") + end_time: Optional[datetime] = Field(default=None, description="Scope the delete to spans starting before this timestamp (exclusive). ISO 8601 format (e.g., `2024-01-02T00:00:00Z`). Each bound is independent: omitting `start_time` defaults to two years ago; omitting `end_time` defaults to now. You may provide either or both. ") + __properties: ClassVar[List[str]] = ["project_id", "span_ids", "start_time", "end_time"] model_config = ConfigDict( populate_by_name=True, @@ -88,7 +91,9 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "project_id": obj.get("project_id"), - "span_ids": obj.get("span_ids") + "span_ids": obj.get("span_ids"), + "start_time": obj.get("start_time"), + "end_time": obj.get("end_time") }) return _obj diff --git a/src/arize/_generated/api_client/models/delete_spans_response.py b/src/arize/_generated/api_client/models/delete_spans_response.py index c8e2fec..21f2920 100644 --- a/src/arize/_generated/api_client/models/delete_spans_response.py +++ b/src/arize/_generated/api_client/models/delete_spans_response.py @@ -81,10 +81,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DeleteSpansResponse) in the input: " + _key) _obj = cls.model_validate({ "completed": obj.get("completed"), diff --git a/src/arize/_generated/api_client/models/dimension.py b/src/arize/_generated/api_client/models/dimension.py index efa46b3..b13e7a2 100644 --- a/src/arize/_generated/api_client/models/dimension.py +++ b/src/arize/_generated/api_client/models/dimension.py @@ -81,10 +81,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in Dimension) in the input: " + _key) _obj = cls.model_validate({ "category": obj.get("category"), diff --git a/src/arize/_generated/api_client/models/downtime_config.py b/src/arize/_generated/api_client/models/downtime_config.py index 20ce19a..9f36d28 100644 --- a/src/arize/_generated/api_client/models/downtime_config.py +++ b/src/arize/_generated/api_client/models/downtime_config.py @@ -82,10 +82,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DowntimeConfig) in the input: " + _key) _obj = cls.model_validate({ "start": obj.get("start"), diff --git a/src/arize/_generated/api_client/models/drift_monitor.py b/src/arize/_generated/api_client/models/drift_monitor.py index bf69d8a..4c39d0b 100644 --- a/src/arize/_generated/api_client/models/drift_monitor.py +++ b/src/arize/_generated/api_client/models/drift_monitor.py @@ -159,10 +159,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DriftMonitor) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/dynamic_range_threshold.py b/src/arize/_generated/api_client/models/dynamic_range_threshold.py index 9a81b56..8a761b0 100644 --- a/src/arize/_generated/api_client/models/dynamic_range_threshold.py +++ b/src/arize/_generated/api_client/models/dynamic_range_threshold.py @@ -97,10 +97,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DynamicRangeThreshold) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/dynamic_single_threshold.py b/src/arize/_generated/api_client/models/dynamic_single_threshold.py index bd3ab28..3ed829d 100644 --- a/src/arize/_generated/api_client/models/dynamic_single_threshold.py +++ b/src/arize/_generated/api_client/models/dynamic_single_threshold.py @@ -91,10 +91,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DynamicSingleThreshold) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/dynamic_threshold_bound.py b/src/arize/_generated/api_client/models/dynamic_threshold_bound.py index e52874d..88736bc 100644 --- a/src/arize/_generated/api_client/models/dynamic_threshold_bound.py +++ b/src/arize/_generated/api_client/models/dynamic_threshold_bound.py @@ -81,10 +81,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in DynamicThresholdBound) in the input: " + _key) _obj = cls.model_validate({ "operator": obj.get("operator"), diff --git a/src/arize/_generated/api_client/models/email_notification_config.py b/src/arize/_generated/api_client/models/email_notification_config.py index a9123f9..a1e5416 100644 --- a/src/arize/_generated/api_client/models/email_notification_config.py +++ b/src/arize/_generated/api_client/models/email_notification_config.py @@ -87,10 +87,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in EmailNotificationConfig) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/experiment.py b/src/arize/_generated/api_client/models/experiment.py index 1338944..333bf54 100644 --- a/src/arize/_generated/api_client/models/experiment.py +++ b/src/arize/_generated/api_client/models/experiment.py @@ -29,13 +29,14 @@ class Experiment(BaseModel): """ # noqa: E501 id: StrictStr = Field(description="Unique identifier for the experiment") name: StrictStr = Field(description="Name of the experiment") + space_id: StrictStr = Field(description="Unique identifier for the space this experiment belongs to") dataset_id: Optional[StrictStr] = Field(default=None, description="Unique identifier for the dataset associated with this experiment. Null if the experiment isn't associated with a dataset.") dataset_version_id: Optional[StrictStr] = Field(default=None, description="Unique identifier for the dataset version associated with this experiment. Null if the experiment isn't associated with a dataset.") created_at: datetime = Field(description="Timestamp for when the experiment was created") updated_at: datetime = Field(description="Timestamp for the last update of the experiment") experiment_traces_project_id: Optional[StrictStr] = Field(default=None, description="Unique identifier for the experiment traces project this experiment belongs to (if it exists)") integration_id: Optional[StrictStr] = Field(default=None, description="Identifier (base64) of the agent integration that backs this experiment, as returned by the integrations API. Null for non-agent experiments (for example, SDK or Playground experiments). ") - __properties: ClassVar[List[str]] = ["id", "name", "dataset_id", "dataset_version_id", "created_at", "updated_at", "experiment_traces_project_id", "integration_id"] + __properties: ClassVar[List[str]] = ["id", "name", "space_id", "dataset_id", "dataset_version_id", "created_at", "updated_at", "experiment_traces_project_id", "integration_id"] model_config = ConfigDict( populate_by_name=True, @@ -102,14 +103,11 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in Experiment) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), "name": obj.get("name"), + "space_id": obj.get("space_id"), "dataset_id": obj.get("dataset_id"), "dataset_version_id": obj.get("dataset_version_id"), "created_at": obj.get("created_at"), diff --git a/src/arize/_generated/api_client/models/experiment_run.py b/src/arize/_generated/api_client/models/experiment_run.py index 69257d2..0aa2f9c 100644 --- a/src/arize/_generated/api_client/models/experiment_run.py +++ b/src/arize/_generated/api_client/models/experiment_run.py @@ -28,7 +28,7 @@ class ExperimentRun(BaseModel): An experiment run with experiment data including outputs, evaluations, and trace metadata """ # noqa: E501 id: StrictStr = Field(description="System-assigned unique ID for the example") - example_id: StrictStr = Field(description="ID of the dataset example associated with this experiment run") + example_id: Optional[StrictStr] = Field(default=None, description="ID of the dataset example associated with this experiment run. Null when the experiment isn't associated with a dataset.") output: Optional[StrictStr] = Field(default=None, description="Output of the task for the matching example. Null when the task errored.") error: Optional[StrictStr] = Field(default=None, description="Error message when the task failed. Null on success.") annotations: Optional[List[Annotation]] = Field(default=None, description="List of human annotations on this experiment run") @@ -94,6 +94,11 @@ def to_dict(self) -> Dict[str, Any]: for _key, _value in self.additional_properties.items(): _dict[_key] = _value + # set to None if example_id (nullable) is None + # and model_fields_set contains the field + if self.example_id is None and "example_id" in self.model_fields_set: + _dict['example_id'] = None + # set to None if output (nullable) is None # and model_fields_set contains the field if self.output is None and "output" in self.model_fields_set: diff --git a/src/arize/_generated/api_client/models/experiment_run_input.py b/src/arize/_generated/api_client/models/experiment_run_input.py index 9f9b107..fa073e5 100644 --- a/src/arize/_generated/api_client/models/experiment_run_input.py +++ b/src/arize/_generated/api_client/models/experiment_run_input.py @@ -18,7 +18,7 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Optional from typing import Optional, Set from typing_extensions import Self @@ -26,7 +26,7 @@ class ExperimentRunInput(BaseModel): """ An experiment run with experiment data including outputs, evaluations, and trace metadata """ # noqa: E501 - example_id: StrictStr = Field(description="ID of the dataset example associated with this experiment run") + example_id: Optional[StrictStr] = Field(default=None, description="ID of the dataset example associated with this experiment run. Provided when the experiment is associated with a dataset; omitted otherwise.") output: StrictStr = Field(description="output of the task for the matching example") additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["example_id", "output"] @@ -77,6 +77,11 @@ def to_dict(self) -> Dict[str, Any]: for _key, _value in self.additional_properties.items(): _dict[_key] = _value + # set to None if example_id (nullable) is None + # and model_fields_set contains the field + if self.example_id is None and "example_id" in self.model_fields_set: + _dict['example_id'] = None + return _dict @classmethod diff --git a/src/arize/_generated/api_client/models/experiment_with_run_ids.py b/src/arize/_generated/api_client/models/experiment_with_run_ids.py index dcaa756..09e7c70 100644 --- a/src/arize/_generated/api_client/models/experiment_with_run_ids.py +++ b/src/arize/_generated/api_client/models/experiment_with_run_ids.py @@ -29,6 +29,7 @@ class ExperimentWithRunIds(BaseModel): """ # noqa: E501 id: StrictStr = Field(description="Unique identifier for the experiment") name: StrictStr = Field(description="Name of the experiment") + space_id: StrictStr = Field(description="Unique identifier for the space this experiment belongs to") dataset_id: Optional[StrictStr] = Field(default=None, description="Unique identifier for the dataset associated with this experiment. Null if the experiment isn't associated with a dataset.") dataset_version_id: Optional[StrictStr] = Field(default=None, description="Unique identifier for the dataset version associated with this experiment. Null if the experiment isn't associated with a dataset.") created_at: datetime = Field(description="Timestamp for when the experiment was created") @@ -36,7 +37,7 @@ class ExperimentWithRunIds(BaseModel): experiment_traces_project_id: Optional[StrictStr] = Field(default=None, description="Unique identifier for the experiment traces project this experiment belongs to (if it exists)") integration_id: Optional[StrictStr] = Field(default=None, description="Identifier (base64) of the agent integration that backs this experiment, as returned by the integrations API. Null for non-agent experiments (for example, SDK or Playground experiments). ") run_ids: List[StrictStr] = Field(description="IDs of the newly inserted experiment runs, in input order.") - __properties: ClassVar[List[str]] = ["id", "name", "dataset_id", "dataset_version_id", "created_at", "updated_at", "experiment_traces_project_id", "integration_id", "run_ids"] + __properties: ClassVar[List[str]] = ["id", "name", "space_id", "dataset_id", "dataset_version_id", "created_at", "updated_at", "experiment_traces_project_id", "integration_id", "run_ids"] model_config = ConfigDict( populate_by_name=True, @@ -103,14 +104,11 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ExperimentWithRunIds) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), "name": obj.get("name"), + "space_id": obj.get("space_id"), "dataset_id": obj.get("dataset_id"), "dataset_version_id": obj.get("dataset_version_id"), "created_at": obj.get("created_at"), diff --git a/src/arize/_generated/api_client/models/fixed_custom_baseline_window.py b/src/arize/_generated/api_client/models/fixed_custom_baseline_window.py index bdfb577..7349c67 100644 --- a/src/arize/_generated/api_client/models/fixed_custom_baseline_window.py +++ b/src/arize/_generated/api_client/models/fixed_custom_baseline_window.py @@ -89,10 +89,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in FixedCustomBaselineWindow) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/integration_notification_config.py b/src/arize/_generated/api_client/models/integration_notification_config.py index 9de7720..e007989 100644 --- a/src/arize/_generated/api_client/models/integration_notification_config.py +++ b/src/arize/_generated/api_client/models/integration_notification_config.py @@ -87,10 +87,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in IntegrationNotificationConfig) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/list_annotation_queue_records_response.py b/src/arize/_generated/api_client/models/list_annotation_queue_records_response.py index 4ebf4ff..2b9ce57 100644 --- a/src/arize/_generated/api_client/models/list_annotation_queue_records_response.py +++ b/src/arize/_generated/api_client/models/list_annotation_queue_records_response.py @@ -92,10 +92,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ListAnnotationQueueRecordsResponse) in the input: " + _key) _obj = cls.model_validate({ "records": [AnnotationQueueRecord.from_dict(_item) for _item in obj["records"]] if obj.get("records") is not None else None, diff --git a/src/arize/_generated/api_client/models/list_annotation_queues_response.py b/src/arize/_generated/api_client/models/list_annotation_queues_response.py index a0940c7..90b1c7a 100644 --- a/src/arize/_generated/api_client/models/list_annotation_queues_response.py +++ b/src/arize/_generated/api_client/models/list_annotation_queues_response.py @@ -92,10 +92,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ListAnnotationQueuesResponse) in the input: " + _key) _obj = cls.model_validate({ "annotation_queues": [AnnotationQueue.from_dict(_item) for _item in obj["annotation_queues"]] if obj.get("annotation_queues") is not None else None, diff --git a/src/arize/_generated/api_client/models/list_dataset_examples_response.py b/src/arize/_generated/api_client/models/list_dataset_examples_response.py index d29a486..d40c632 100644 --- a/src/arize/_generated/api_client/models/list_dataset_examples_response.py +++ b/src/arize/_generated/api_client/models/list_dataset_examples_response.py @@ -92,10 +92,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ListDatasetExamplesResponse) in the input: " + _key) _obj = cls.model_validate({ "examples": [DatasetExample.from_dict(_item) for _item in obj["examples"]] if obj.get("examples") is not None else None, diff --git a/src/arize/_generated/api_client/models/list_datasets_response.py b/src/arize/_generated/api_client/models/list_datasets_response.py index 5a3e0a1..327d08b 100644 --- a/src/arize/_generated/api_client/models/list_datasets_response.py +++ b/src/arize/_generated/api_client/models/list_datasets_response.py @@ -92,10 +92,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ListDatasetsResponse) in the input: " + _key) _obj = cls.model_validate({ "datasets": [Dataset.from_dict(_item) for _item in obj["datasets"]] if obj.get("datasets") is not None else None, diff --git a/src/arize/_generated/api_client/models/list_experiment_runs_response.py b/src/arize/_generated/api_client/models/list_experiment_runs_response.py index 594ff7d..7f8f62e 100644 --- a/src/arize/_generated/api_client/models/list_experiment_runs_response.py +++ b/src/arize/_generated/api_client/models/list_experiment_runs_response.py @@ -92,10 +92,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ListExperimentRunsResponse) in the input: " + _key) _obj = cls.model_validate({ "experiment_runs": [ExperimentRun.from_dict(_item) for _item in obj["experiment_runs"]] if obj.get("experiment_runs") is not None else None, diff --git a/src/arize/_generated/api_client/models/list_experiments_response.py b/src/arize/_generated/api_client/models/list_experiments_response.py index 3fc70ae..094624c 100644 --- a/src/arize/_generated/api_client/models/list_experiments_response.py +++ b/src/arize/_generated/api_client/models/list_experiments_response.py @@ -92,10 +92,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ListExperimentsResponse) in the input: " + _key) _obj = cls.model_validate({ "experiments": [Experiment.from_dict(_item) for _item in obj["experiments"]] if obj.get("experiments") is not None else None, diff --git a/src/arize/_generated/api_client/models/list_projects_response.py b/src/arize/_generated/api_client/models/list_projects_response.py index a41c771..bda726d 100644 --- a/src/arize/_generated/api_client/models/list_projects_response.py +++ b/src/arize/_generated/api_client/models/list_projects_response.py @@ -92,10 +92,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ListProjectsResponse) in the input: " + _key) _obj = cls.model_validate({ "projects": [Project.from_dict(_item) for _item in obj["projects"]] if obj.get("projects") is not None else None, diff --git a/src/arize/_generated/api_client/models/list_spans_response.py b/src/arize/_generated/api_client/models/list_spans_response.py index 8c6ccda..9a88501 100644 --- a/src/arize/_generated/api_client/models/list_spans_response.py +++ b/src/arize/_generated/api_client/models/list_spans_response.py @@ -92,10 +92,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ListSpansResponse) in the input: " + _key) _obj = cls.model_validate({ "spans": [Span.from_dict(_item) for _item in obj["spans"]] if obj.get("spans") is not None else None, diff --git a/src/arize/_generated/api_client/models/list_traces_response.py b/src/arize/_generated/api_client/models/list_traces_response.py index 60d418c..29458a3 100644 --- a/src/arize/_generated/api_client/models/list_traces_response.py +++ b/src/arize/_generated/api_client/models/list_traces_response.py @@ -92,10 +92,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ListTracesResponse) in the input: " + _key) _obj = cls.model_validate({ "traces": [Trace.from_dict(_item) for _item in obj["traces"]] if obj.get("traces") is not None else None, diff --git a/src/arize/_generated/api_client/models/manual_range_threshold.py b/src/arize/_generated/api_client/models/manual_range_threshold.py index f43eb3f..7131501 100644 --- a/src/arize/_generated/api_client/models/manual_range_threshold.py +++ b/src/arize/_generated/api_client/models/manual_range_threshold.py @@ -95,10 +95,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ManualRangeThreshold) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/manual_single_threshold.py b/src/arize/_generated/api_client/models/manual_single_threshold.py index 39e15e4..d5378c0 100644 --- a/src/arize/_generated/api_client/models/manual_single_threshold.py +++ b/src/arize/_generated/api_client/models/manual_single_threshold.py @@ -89,10 +89,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ManualSingleThreshold) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/manual_threshold_bound.py b/src/arize/_generated/api_client/models/manual_threshold_bound.py index 4c1e6aa..5c63e5f 100644 --- a/src/arize/_generated/api_client/models/manual_threshold_bound.py +++ b/src/arize/_generated/api_client/models/manual_threshold_bound.py @@ -81,10 +81,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ManualThresholdBound) in the input: " + _key) _obj = cls.model_validate({ "operator": obj.get("operator"), diff --git a/src/arize/_generated/api_client/models/model_baseline_config.py b/src/arize/_generated/api_client/models/model_baseline_config.py index 5e80db5..1975175 100644 --- a/src/arize/_generated/api_client/models/model_baseline_config.py +++ b/src/arize/_generated/api_client/models/model_baseline_config.py @@ -95,10 +95,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ModelBaselineConfig) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/monitor_base.py b/src/arize/_generated/api_client/models/monitor_base.py index 1503117..430a266 100644 --- a/src/arize/_generated/api_client/models/monitor_base.py +++ b/src/arize/_generated/api_client/models/monitor_base.py @@ -140,10 +140,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in MonitorBase) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/monitor_filter.py b/src/arize/_generated/api_client/models/monitor_filter.py index 4a3edf2..e3cb703 100644 --- a/src/arize/_generated/api_client/models/monitor_filter.py +++ b/src/arize/_generated/api_client/models/monitor_filter.py @@ -86,10 +86,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in MonitorFilter) in the input: " + _key) _obj = cls.model_validate({ "dimension": Dimension.from_dict(obj["dimension"]) if obj.get("dimension") is not None else None, diff --git a/src/arize/_generated/api_client/models/moving_custom_baseline_window.py b/src/arize/_generated/api_client/models/moving_custom_baseline_window.py index f36c9ca..0b307a5 100644 --- a/src/arize/_generated/api_client/models/moving_custom_baseline_window.py +++ b/src/arize/_generated/api_client/models/moving_custom_baseline_window.py @@ -88,10 +88,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in MovingCustomBaselineWindow) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/models/pagination_metadata.py b/src/arize/_generated/api_client/models/pagination_metadata.py index 7367908..e727c19 100644 --- a/src/arize/_generated/api_client/models/pagination_metadata.py +++ b/src/arize/_generated/api_client/models/pagination_metadata.py @@ -80,10 +80,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in PaginationMetadata) in the input: " + _key) _obj = cls.model_validate({ "next_cursor": obj.get("next_cursor"), diff --git a/src/arize/_generated/api_client/models/performance_monitor.py b/src/arize/_generated/api_client/models/performance_monitor.py index 6306a30..e9538d2 100644 --- a/src/arize/_generated/api_client/models/performance_monitor.py +++ b/src/arize/_generated/api_client/models/performance_monitor.py @@ -151,10 +151,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in PerformanceMonitor) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/problem.py b/src/arize/_generated/api_client/models/problem.py index fea7fe1..16bebc6 100644 --- a/src/arize/_generated/api_client/models/problem.py +++ b/src/arize/_generated/api_client/models/problem.py @@ -83,10 +83,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in Problem) in the input: " + _key) _obj = cls.model_validate({ "title": obj.get("title"), diff --git a/src/arize/_generated/api_client/models/project.py b/src/arize/_generated/api_client/models/project.py index 16b8ef3..5493f10 100644 --- a/src/arize/_generated/api_client/models/project.py +++ b/src/arize/_generated/api_client/models/project.py @@ -83,10 +83,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in Project) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/scheduled_runtime_config.py b/src/arize/_generated/api_client/models/scheduled_runtime_config.py index fbfc639..55324cc 100644 --- a/src/arize/_generated/api_client/models/scheduled_runtime_config.py +++ b/src/arize/_generated/api_client/models/scheduled_runtime_config.py @@ -82,10 +82,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in ScheduledRuntimeConfig) in the input: " + _key) _obj = cls.model_validate({ "enabled": obj.get("enabled"), diff --git a/src/arize/_generated/api_client/models/span.py b/src/arize/_generated/api_client/models/span.py index 7ef1b9b..1dcabdc 100644 --- a/src/arize/_generated/api_client/models/span.py +++ b/src/arize/_generated/api_client/models/span.py @@ -129,10 +129,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in Span) in the input: " + _key) _obj = cls.model_validate({ "name": obj.get("name"), diff --git a/src/arize/_generated/api_client/models/span_context.py b/src/arize/_generated/api_client/models/span_context.py index 40a984b..2aa3d5e 100644 --- a/src/arize/_generated/api_client/models/span_context.py +++ b/src/arize/_generated/api_client/models/span_context.py @@ -80,10 +80,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in SpanContext) in the input: " + _key) _obj = cls.model_validate({ "trace_id": obj.get("trace_id"), diff --git a/src/arize/_generated/api_client/models/span_event.py b/src/arize/_generated/api_client/models/span_event.py index 746d989..942469d 100644 --- a/src/arize/_generated/api_client/models/span_event.py +++ b/src/arize/_generated/api_client/models/span_event.py @@ -82,10 +82,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in SpanEvent) in the input: " + _key) _obj = cls.model_validate({ "name": obj.get("name"), diff --git a/src/arize/_generated/api_client/models/task_run.py b/src/arize/_generated/api_client/models/task_run.py index 2904911..7dc7c6b 100644 --- a/src/arize/_generated/api_client/models/task_run.py +++ b/src/arize/_generated/api_client/models/task_run.py @@ -42,6 +42,7 @@ class TaskRun(BaseModel): created_at: datetime = Field(description="When the run was created.") created_by_user_id: Optional[StrictStr] = Field(description="The unique identifier for the user who triggered the run.") failure_reason: Optional[StrictStr] = Field(default=None, description="Human-readable explanation of why the run failed or was cancelled; null for successful runs. For example, when all matching data already has evaluation labels from a previous run, the task cancels with zero successes, errors, and skipped items, and this field explains that the task must be re-triggered with `override_evaluations` enabled to re-evaluate it. ") + additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["id", "task_id", "experiment_id", "status", "run_started_at", "run_finished_at", "data_start_time", "data_end_time", "num_successes", "num_errors", "num_skipped", "created_at", "created_by_user_id", "failure_reason"] model_config = ConfigDict( @@ -74,8 +75,10 @@ def to_dict(self) -> Dict[str, Any]: * `None` is only added to the output dict for nullable fields that were set at model initialization. Other fields with value `None` are ignored. + * Fields in `self.additional_properties` are added to the output dict. """ excluded_fields: Set[str] = set([ + "additional_properties", ]) _dict = self.model_dump( @@ -83,6 +86,11 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # puts key-value pairs in additional_properties in the top level + if self.additional_properties is not None: + for _key, _value in self.additional_properties.items(): + _dict[_key] = _value + # set to None if experiment_id (nullable) is None # and model_fields_set contains the field if self.experiment_id is None and "experiment_id" in self.model_fields_set: @@ -129,11 +137,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in TaskRun) in the input: " + _key) - _obj = cls.model_validate({ "id": obj.get("id"), "task_id": obj.get("task_id"), @@ -150,6 +153,11 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "created_by_user_id": obj.get("created_by_user_id"), "failure_reason": obj.get("failure_reason") }) + # store additional fields in additional_properties + for _key in obj.keys(): + if _key not in cls.__properties: + _obj.additional_properties[_key] = obj.get(_key) + return _obj diff --git a/src/arize/_generated/api_client/models/trace.py b/src/arize/_generated/api_client/models/trace.py index b3e518d..8a39524 100644 --- a/src/arize/_generated/api_client/models/trace.py +++ b/src/arize/_generated/api_client/models/trace.py @@ -93,10 +93,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in Trace) in the input: " + _key) _obj = cls.model_validate({ "trace_id": obj.get("trace_id"), diff --git a/src/arize/_generated/api_client/models/tracing_monitor.py b/src/arize/_generated/api_client/models/tracing_monitor.py index 9108d54..71fc9eb 100644 --- a/src/arize/_generated/api_client/models/tracing_monitor.py +++ b/src/arize/_generated/api_client/models/tracing_monitor.py @@ -153,10 +153,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in TracingMonitor) in the input: " + _key) _obj = cls.model_validate({ "id": obj.get("id"), diff --git a/src/arize/_generated/api_client/models/update_annotation_queue_request.py b/src/arize/_generated/api_client/models/update_annotation_queue_request.py index 94ba53b..373c1d3 100644 --- a/src/arize/_generated/api_client/models/update_annotation_queue_request.py +++ b/src/arize/_generated/api_client/models/update_annotation_queue_request.py @@ -28,7 +28,7 @@ class UpdateAnnotationQueueRequest(BaseModel): UpdateAnnotationQueueRequest """ # noqa: E501 name: Optional[Annotated[str, Field(min_length=1, strict=True, max_length=255)]] = Field(default=None, description="The name of the annotation queue. Must be unique within the space. ") - instructions: Optional[Annotated[str, Field(strict=True, max_length=5000)]] = Field(default=None, description="The instructions for annotators working on this queue. Send an empty string to clear the instructions. ") + instructions: Optional[Annotated[str, Field(min_length=1, strict=True, max_length=5000)]] = Field(default=None, description="The instructions for annotators working on this queue. Set to `null` to clear the instructions. ") annotation_config_ids: Optional[List[StrictStr]] = Field(default=None, description="The full list of annotation config IDs to associate with this queue. This replaces all existing annotation config associations. All annotation configs must belong to the same space as the queue. ") annotator_emails: Optional[List[StrictStr]] = Field(default=None, description="The full list of user emails to assign to this queue. This replaces all existing user assignments. All users must have an active account and access to the queue's space. ") __properties: ClassVar[List[str]] = ["name", "instructions", "annotation_config_ids", "annotator_emails"] @@ -72,6 +72,11 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # set to None if instructions (nullable) is None + # and model_fields_set contains the field + if self.instructions is None and "instructions" in self.model_fields_set: + _dict['instructions'] = None + return _dict @classmethod diff --git a/src/arize/_generated/api_client/models/update_llm_config.py b/src/arize/_generated/api_client/models/update_llm_config.py index 5303f89..26d5f08 100644 --- a/src/arize/_generated/api_client/models/update_llm_config.py +++ b/src/arize/_generated/api_client/models/update_llm_config.py @@ -29,8 +29,8 @@ class UpdateLlmConfig(BaseModel): Partial LLM config for PATCH. `provider` is immutable; if present it must match the stored value. Field applicability is provider-specific and enforced by the handler with 422: `api_key` and `is_function_calling_enabled` do not apply to `AWS_BEDROCK` or `VERTEX_AI`; `auth` applies to `AWS_BEDROCK` only; `base_url` and `headers` apply to `CUSTOM` and `NVIDIA_NIM` only; `is_default_models_enabled` and `model_names` apply to `AWS_BEDROCK`, `CUSTOM`, and `NVIDIA_NIM` only; `project_id`, `location`, and `project_access_label` apply to `VERTEX_AI` only. """ # noqa: E501 provider: Optional[LlmIntegrationProvider] = None - api_key: Optional[StrictStr] = Field(default=None, description="Rotate the API key. Pass null to clear it. Omit to keep unchanged. Not valid for `AWS_BEDROCK` (bearer tokens are rotated via `auth`).") - is_function_calling_enabled: Optional[StrictBool] = Field(default=None, description="Enable or disable function/tool calling. Omit to keep unchanged. Not valid for `AWS_BEDROCK`.") + api_key: Optional[StrictStr] = Field(default=None, description="Rotate the API key. Pass null to clear it. Omit to keep unchanged. Not valid for `AWS_BEDROCK` (bearer tokens are rotated via `auth`) or `VERTEX_AI`.") + is_function_calling_enabled: Optional[StrictBool] = Field(default=None, description="Enable or disable function/tool calling. Omit to keep unchanged. Not valid for `AWS_BEDROCK` or `VERTEX_AI`.") auth: Optional[CreateAwsBedrockAuth] = None base_url: Optional[StrictStr] = Field(default=None, description="(`CUSTOM` and `NVIDIA_NIM` only) New endpoint URL. For `NVIDIA_NIM` the field is optional on the resource, so null clears it (falling back to the provider default endpoint). For `CUSTOM` it is required on the resource — null is rejected with 422. Omit to keep unchanged.") headers: Optional[Dict[str, StrictStr]] = Field(default=None, description="(`CUSTOM` and `NVIDIA_NIM` only) Replaces the configured custom request headers: the provided map becomes the full header set. Pass null to clear all headers. Omit to keep unchanged. Write-only; names are exposed as `header_names` on read. The serialized header map must not exceed 8,175 bytes.") diff --git a/src/arize/_generated/api_client/models/update_organization_request.py b/src/arize/_generated/api_client/models/update_organization_request.py index 56ca1d2..900595d 100644 --- a/src/arize/_generated/api_client/models/update_organization_request.py +++ b/src/arize/_generated/api_client/models/update_organization_request.py @@ -28,7 +28,7 @@ class UpdateOrganizationRequest(BaseModel): UpdateOrganizationRequest """ # noqa: E501 name: Optional[Annotated[str, Field(min_length=1, strict=True, max_length=255)]] = Field(default=None, description="Updated name for the organization (must be unique within the account)") - description: Optional[Annotated[str, Field(strict=True, max_length=1000)]] = Field(default=None, description="Updated description for the organization. Set to an empty string to clear it.") + description: Optional[Annotated[str, Field(strict=True, max_length=1000)]] = Field(default=None, description="Updated description for the organization. Set to `null` to clear it.") __properties: ClassVar[List[str]] = ["name", "description"] model_config = ConfigDict( @@ -70,6 +70,11 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # set to None if description (nullable) is None + # and model_fields_set contains the field + if self.description is None and "description" in self.model_fields_set: + _dict['description'] = None + return _dict @classmethod diff --git a/src/arize/_generated/api_client/models/update_role_request.py b/src/arize/_generated/api_client/models/update_role_request.py index 5fb2df7..98e68f9 100644 --- a/src/arize/_generated/api_client/models/update_role_request.py +++ b/src/arize/_generated/api_client/models/update_role_request.py @@ -29,7 +29,7 @@ class UpdateRoleRequest(BaseModel): UpdateRoleRequest """ # noqa: E501 name: Optional[Annotated[str, Field(strict=True, max_length=255)]] = Field(default=None, description="Updated name for the role. Must be unique within the account.") - description: Optional[Annotated[str, Field(strict=True, max_length=1000)]] = Field(default=None, description="Updated description of the role.") + description: Optional[Annotated[str, Field(strict=True, max_length=1000)]] = Field(default=None, description="Updated description of the role. Set to `null` to clear it.") permissions: Optional[Annotated[List[Permission], Field(min_length=1)]] = Field(default=None, description="Replacement set of permissions. When provided, the existing permissions are fully replaced. Each value must be a valid permission identifier. ") __properties: ClassVar[List[str]] = ["name", "description", "permissions"] @@ -72,6 +72,11 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # set to None if description (nullable) is None + # and model_fields_set contains the field + if self.description is None and "description" in self.model_fields_set: + _dict['description'] = None + return _dict @classmethod diff --git a/src/arize/_generated/api_client/models/update_space_request.py b/src/arize/_generated/api_client/models/update_space_request.py index 3f8fffd..c5be7d8 100644 --- a/src/arize/_generated/api_client/models/update_space_request.py +++ b/src/arize/_generated/api_client/models/update_space_request.py @@ -28,7 +28,7 @@ class UpdateSpaceRequest(BaseModel): UpdateSpaceRequest """ # noqa: E501 name: Optional[Annotated[str, Field(strict=True, max_length=255)]] = Field(default=None, description="Updated name of the space") - description: Optional[Annotated[str, Field(strict=True, max_length=1000)]] = Field(default=None, description="Updated description of the space") + description: Optional[Annotated[str, Field(strict=True, max_length=1000)]] = Field(default=None, description="Updated description of the space. Set to `null` to clear it.") is_private: Optional[StrictBool] = Field(default=None, description="Updated visibility for the space. Set to `true` to make the space private (visible only to members and admins), or `false` to make it public. When omitted, the existing visibility is preserved. ") __properties: ClassVar[List[str]] = ["name", "description", "is_private"] @@ -71,6 +71,11 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) + # set to None if description (nullable) is None + # and model_fields_set contains the field + if self.description is None and "description" in self.model_fields_set: + _dict['description'] = None + return _dict @classmethod diff --git a/src/arize/_generated/api_client/models/webhook_notification_config.py b/src/arize/_generated/api_client/models/webhook_notification_config.py index abdda0b..09bf404 100644 --- a/src/arize/_generated/api_client/models/webhook_notification_config.py +++ b/src/arize/_generated/api_client/models/webhook_notification_config.py @@ -93,10 +93,6 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: if not isinstance(obj, dict): return cls.model_validate(obj) - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in WebhookNotificationConfig) in the input: " + _key) _obj = cls.model_validate({ "type": obj.get("type"), diff --git a/src/arize/_generated/api_client/test/test_delete_spans_request.py b/src/arize/_generated/api_client/test/test_delete_spans_request.py index 7775164..1ad07dc 100644 --- a/src/arize/_generated/api_client/test/test_delete_spans_request.py +++ b/src/arize/_generated/api_client/test/test_delete_spans_request.py @@ -38,7 +38,9 @@ def make_instance(self, include_optional) -> DeleteSpansRequest: project_id = '', span_ids = [ '' - ] + ], + start_time = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + end_time = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f') ) else: return DeleteSpansRequest( diff --git a/src/arize/_generated/api_client/test/test_experiment.py b/src/arize/_generated/api_client/test/test_experiment.py index 98d82f8..4f009e2 100644 --- a/src/arize/_generated/api_client/test/test_experiment.py +++ b/src/arize/_generated/api_client/test/test_experiment.py @@ -37,6 +37,7 @@ def make_instance(self, include_optional) -> Experiment: return Experiment( id = '', name = '', + space_id = '', dataset_id = '', dataset_version_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), @@ -48,6 +49,7 @@ def make_instance(self, include_optional) -> Experiment: return Experiment( id = '', name = '', + space_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ) diff --git a/src/arize/_generated/api_client/test/test_experiment_with_run_ids.py b/src/arize/_generated/api_client/test/test_experiment_with_run_ids.py index 289f9dc..37fa516 100644 --- a/src/arize/_generated/api_client/test/test_experiment_with_run_ids.py +++ b/src/arize/_generated/api_client/test/test_experiment_with_run_ids.py @@ -37,6 +37,7 @@ def make_instance(self, include_optional) -> ExperimentWithRunIds: return ExperimentWithRunIds( id = '', name = '', + space_id = '', dataset_id = '', dataset_version_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), @@ -51,6 +52,7 @@ def make_instance(self, include_optional) -> ExperimentWithRunIds: return ExperimentWithRunIds( id = '', name = '', + space_id = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), run_ids = [ diff --git a/src/arize/ai_integrations/client.py b/src/arize/ai_integrations/client.py index e80ddda..10c2fcf 100644 --- a/src/arize/ai_integrations/client.py +++ b/src/arize/ai_integrations/client.py @@ -11,6 +11,7 @@ _find_ai_integration_id, _resolve_resource, ) +from arize.utils.unset import _UNSET, UNSET, is_provided if TYPE_CHECKING: # builtins is needed to use builtins.list in type annotations because @@ -31,13 +32,6 @@ logger = logging.getLogger(__name__) -# Sentinel object used to distinguish "caller did not pass this argument" from -# "caller explicitly passed None" in update(). This matters because the -# generated pydantic model uses ``model_fields_set`` to decide whether to -# serialize a nullable field as JSON ``null`` (clearing it on the server) vs. -# omitting it entirely (leaving it unchanged). -_UNSET: Any = object() - class AiIntegrationsClient: """Client for managing Arize AI integrations. @@ -211,19 +205,20 @@ def update( *, integration: str, space: str | None = None, - name: str | None = _UNSET, - provider: AiIntegrationProvider | None = _UNSET, - api_key: str | None = _UNSET, - base_url: str | None = _UNSET, - model_names: builtins.list[str] | None = _UNSET, - headers: dict[str, str] | None = _UNSET, - enable_default_models: bool | None = _UNSET, - function_calling_enabled: bool | None = _UNSET, - auth_type: AiIntegrationAuthType | None = _UNSET, + name: str | None | UNSET = _UNSET, + provider: AiIntegrationProvider | None | UNSET = _UNSET, + api_key: str | None | UNSET = _UNSET, + base_url: str | None | UNSET = _UNSET, + model_names: builtins.list[str] | None | UNSET = _UNSET, + headers: dict[str, str] | None | UNSET = _UNSET, + enable_default_models: bool | None | UNSET = _UNSET, + function_calling_enabled: bool | None | UNSET = _UNSET, + auth_type: AiIntegrationAuthType | None | UNSET = _UNSET, provider_metadata: AwsProviderMetadata | GcpProviderMetadata - | None = _UNSET, - scopings: builtins.list[AiIntegrationScoping] | None = _UNSET, + | None + | UNSET = _UNSET, + scopings: builtins.list[AiIntegrationScoping] | None | UNSET = _UNSET, ) -> AiIntegration: """Update an AI integration by name or ID. @@ -258,36 +253,33 @@ def update( """ from arize._generated import api_client as gen - wrapped_metadata: Any = _UNSET - if provider_metadata is not _UNSET: - wrapped_metadata = ( + kwargs: dict[str, Any] = {} + if is_provided(name) and name is not None: + kwargs["name"] = name + if is_provided(provider): + kwargs["provider"] = provider + if is_provided(api_key): + kwargs["api_key"] = api_key + if is_provided(base_url): + kwargs["base_url"] = base_url + if is_provided(model_names): + kwargs["model_names"] = model_names + if is_provided(headers): + kwargs["headers"] = headers + if is_provided(enable_default_models): + kwargs["enable_default_models"] = enable_default_models + if is_provided(function_calling_enabled): + kwargs["function_calling_enabled"] = function_calling_enabled + if is_provided(auth_type): + kwargs["auth_type"] = auth_type + if is_provided(provider_metadata): + kwargs["provider_metadata"] = ( gen.ProviderMetadata(actual_instance=provider_metadata) if provider_metadata is not None else None ) - - # Build kwargs with only the fields the caller actually provided so - # that pydantic's model_fields_set accurately reflects intent. This - # prevents nullable fields (api_key, base_url, headers, - # provider_metadata) from being serialized as JSON null when the - # caller didn't mention them. - kwargs: dict[str, Any] = { - k: v - for k, v in ( - ("name", name), - ("provider", provider), - ("api_key", api_key), - ("base_url", base_url), - ("model_names", model_names), - ("headers", headers), - ("enable_default_models", enable_default_models), - ("function_calling_enabled", function_calling_enabled), - ("auth_type", auth_type), - ("provider_metadata", wrapped_metadata), - ("scopings", scopings), - ) - if v is not _UNSET - } + if is_provided(scopings): + kwargs["scopings"] = scopings integration_id = _find_ai_integration_id( api=self._api, diff --git a/src/arize/annotation_queues/client.py b/src/arize/annotation_queues/client.py index 7a6058c..a5ecc07 100644 --- a/src/arize/annotation_queues/client.py +++ b/src/arize/annotation_queues/client.py @@ -17,6 +17,7 @@ _find_space_id, _resolve_resource, ) +from arize.utils.unset import _UNSET, UNSET, is_provided if TYPE_CHECKING: import builtins @@ -250,10 +251,10 @@ def update( *, annotation_queue: str, space: str | None = None, - name: str | None = None, - instructions: str | None = None, - annotation_config_ids: builtins.list[str] | None = None, - annotator_emails: builtins.list[str] | None = None, + name: str | None | UNSET = _UNSET, + instructions: str | None | UNSET = _UNSET, + annotation_config_ids: builtins.list[str] | None | UNSET = _UNSET, + annotator_emails: builtins.list[str] | None | UNSET = _UNSET, ) -> AnnotationQueue: """Update an annotation queue. @@ -267,8 +268,8 @@ def update( space: Space ID or name. Required when *annotation_queue* is a name so it can be resolved to an ID. name: New name for the queue (must remain unique within the space). - instructions: New instructions for annotators. Pass an empty string - to clear existing instructions. + instructions: New instructions for annotators. Pass ``None`` to clear + existing instructions. Empty strings are rejected by the server. annotation_config_ids: Full replacement list of annotation config IDs. Pass an empty list to clear. annotator_emails: Full replacement list of annotator emails. @@ -283,16 +284,18 @@ def update( ApiException: If the REST API returns an error response (e.g. 400/401/403/404/409/429). """ - kwargs: dict[str, Any] = { - k: v - for k, v in { - "name": name, - "instructions": instructions, - "annotation_config_ids": annotation_config_ids, - "annotator_emails": annotator_emails, - }.items() - if v is not None - } + kwargs: dict[str, Any] = {} + if is_provided(name) and name is not None: + kwargs["name"] = name + if is_provided(instructions): + kwargs["instructions"] = instructions + if ( + is_provided(annotation_config_ids) + and annotation_config_ids is not None + ): + kwargs["annotation_config_ids"] = annotation_config_ids + if is_provided(annotator_emails) and annotator_emails is not None: + kwargs["annotator_emails"] = annotator_emails if not kwargs: raise ValueError( "At least one of 'name', 'instructions', 'annotation_config_ids'," diff --git a/src/arize/client.py b/src/arize/client.py index 3ca5151..551b8d4 100644 --- a/src/arize/client.py +++ b/src/arize/client.py @@ -19,6 +19,7 @@ from arize.datasets.client import DatasetsClient from arize.evaluators.client import EvaluatorsClient from arize.experiments.client import ExperimentsClient + from arize.integrations.client import IntegrationsClient from arize.ml.client import MLModelsClient from arize.organizations.client import OrganizationsClient from arize.projects.client import ProjectsClient @@ -84,6 +85,10 @@ class ArizeClient(LazySubclientsMixin): "arize.ai_integrations.client", "AiIntegrationsClient", ), + "integrations": ( + "arize.integrations.client", + "IntegrationsClient", + ), "audit_logs": ( "arize.audit_logs.client", "AuditLogsClient", @@ -363,6 +368,11 @@ def ai_integrations(self) -> AiIntegrationsClient: """Access the AI integrations client for managing LLM provider integrations (lazy-loaded).""" return cast("AiIntegrationsClient", self.__getattr__("ai_integrations")) + @property + def integrations(self) -> IntegrationsClient: + """Access the integrations client for LLM and agent integrations (lazy-loaded).""" + return cast("IntegrationsClient", self.__getattr__("integrations")) + @property def datasets(self) -> DatasetsClient: """Access the datasets client for dataset operations (lazy-loaded).""" diff --git a/src/arize/evaluators/client.py b/src/arize/evaluators/client.py index 7c04b68..44bdfef 100644 --- a/src/arize/evaluators/client.py +++ b/src/arize/evaluators/client.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from arize._generated.api_client.models.evaluator_version_code import ( EvaluatorVersionCode as _GenEvaluatorVersionCode, @@ -24,6 +24,7 @@ _find_space_id, _resolve_resource, ) +from arize.utils.unset import _UNSET, UNSET, is_provided if TYPE_CHECKING: from arize._generated.api_client.api_client import ApiClient @@ -302,8 +303,8 @@ def update( *, evaluator: str, space: str | None = None, - name: str | None = None, - description: str | None = None, + name: str | None | UNSET = _UNSET, + description: str | None | UNSET = _UNSET, ) -> Evaluator: """Update an evaluator's metadata. @@ -311,8 +312,10 @@ def update( evaluator: Evaluator name or identifier (base64) to update. space: Optional space name or ID. Required when ``evaluator`` is a name rather than an ID. - name: New evaluator name (must be unique within its space). - description: New description for the evaluator. + name: New evaluator name (must be unique within its space). Omit it + or pass ``None`` to leave the existing name unchanged. + description: New description for the evaluator. Omit to leave + unchanged; pass ``None`` to clear it. Returns: The updated evaluator. @@ -328,7 +331,12 @@ def update( from arize._generated import api_client as gen - body = gen.UpdateEvaluatorRequest(name=name, description=description) + kwargs: dict[str, Any] = {} + if is_provided(name) and name is not None: + kwargs["name"] = name + if is_provided(description): + kwargs["description"] = description + body = gen.UpdateEvaluatorRequest(**kwargs) return self._api.update_evaluator( evaluator_id=evaluator_id, update_evaluator_request=body, diff --git a/src/arize/experiments/client.py b/src/arize/experiments/client.py index 58fb71b..c5841c9 100644 --- a/src/arize/experiments/client.py +++ b/src/arize/experiments/client.py @@ -37,6 +37,7 @@ from arize.utils.resolve import ( _find_dataset_id, _find_experiment_id, + _find_space_id, ) from arize.utils.size import get_payload_size_mb @@ -90,9 +91,9 @@ def __init__( # Use the provided client directly for both APIs self._api = gen.ExperimentsApi(generated_client) - # TODO(Kiko): Space ID should not be needed, - # should work on server tech debt to remove this self._datasets_api = gen.DatasetsApi(generated_client) + # Used to resolve `space` for experiments not associated with a dataset. + self._spaces_api = gen.SpacesApi(generated_client) @prerelease_endpoint(key="experiments.list", stage=ReleaseStage.BETA) def list( @@ -105,11 +106,21 @@ def list( ) -> ListExperimentsResponse: """List experiments the user has access to. - To filter experiments by the dataset they were run on, provide `dataset`. + Narrows the results by whichever scope is given: + - ``dataset``: only experiments run on that dataset. + - ``space``: every experiment in that space, both those associated + with a dataset and those without one. + - neither: every experiment across all spaces the caller can read. + + Passing both applies the narrower ``dataset`` scope, with ``space`` + used only to resolve a dataset name. Args: dataset: Optional dataset name or ID to filter experiments. - space: Optional space name or ID used to resolve ``dataset`` by name. + space: Optional space name or ID. Filters to that space when + ``dataset`` is omitted — the only way to list experiments that + aren't associated with a dataset — and resolves ``dataset`` + when it is a name. limit: Maximum number of experiments to return. The server enforces an upper bound. cursor: Opaque pagination cursor returned from a previous response. @@ -121,17 +132,23 @@ def list( ApiException: If the REST API returns an error response (e.g. 401/403/429). """ - dataset_id = ( - _find_dataset_id( + # Callers may pass both `dataset` and `space`, but the endpoint rejects + # `dataset_id` and `space_id` together — so resolve to exactly one. + # `dataset` is the narrower scope and wins; `space` then only resolves + # its name. + dataset_id: str | None = None + space_id: str | None = None + if dataset is not None: + dataset_id = _find_dataset_id( api=self._datasets_api, dataset=dataset, space=space, ) - if dataset - else None - ) + elif space is not None: + space_id = _find_space_id(api=self._spaces_api, space=space) return self._api.list_experiments( dataset_id=dataset_id, + space_id=space_id, limit=limit, cursor=cursor, ) @@ -141,7 +158,7 @@ def create( self, *, name: str, - dataset: str, + dataset: str | None = None, space: str | None = None, experiment_runs: builtins.list[dict[str, object]] | pd.DataFrame, task_fields: ExperimentTaskFieldNames, @@ -150,8 +167,15 @@ def create( ) -> Experiment: """Create an experiment with one or more experiment runs. + An experiment belongs to a space and may optionally be associated + with a dataset. Provide exactly one of: + - `dataset`: associates the experiment with a dataset; runs may + reference the dataset's examples via `task_fields.example_id`. + - `space`: creates a experiment directly in that space. + Experiments are composed of runs. Each run must include: - - `example_id`: ID of an existing example in the dataset/version + - `example_id`: ID of an existing example in the dataset/version. + Required only when `dataset` is provided. - `output`: Model/task output for the matching example You may include any additional user-defined fields per run (e.g. `model`, @@ -165,11 +189,18 @@ def create( - If the payload is below the configured REST payload threshold (or `force_http=True`), this method uploads via REST. - Otherwise, it attempts a more efficient upload path via gRPC + Flight. + Experiments not associated with a dataset always upload via REST, since + the gRPC + Flight path only supports dataset-associated experiments. Args: - name: Experiment name. Must be unique within the target dataset. - dataset: Dataset name or ID to attach the experiment to. - space: Optional space name or ID used to resolve ``dataset`` by name. + name: Experiment name. Must be unique within the target dataset + or space for an experiment not associated with a dataset. + dataset: Dataset name or ID to attach the experiment to. Provide + `space` instead for an experiment not associated with a dataset. + space: For a dataset-associated experiment, an optional space + name or ID used to resolve `dataset` by name. For an experiment + not associated with a dataset, the space name or ID to create it in; + required when `dataset` is not provided. experiment_runs: Experiment runs either as: - a list of JSON-like dicts, or - a :class:`pandas.DataFrame`. @@ -183,17 +214,30 @@ def create( The created experiment object. Raises: + ValueError: If neither `dataset` nor `space` is provided. TypeError: If `experiment_runs` is not a list of dicts or a DataFrame. RuntimeError: If the Flight upload path is selected and the Flight request fails. ApiException: If the REST API returns an error response (e.g. 400/401/403/409/429). """ - dataset_id = _find_dataset_id( - api=self._datasets_api, - dataset=dataset, - space=space, - ) + space_id: str | None = None + if dataset is not None: + dataset_id: str | None = _find_dataset_id( + api=self._datasets_api, + dataset=dataset, + space=space, + ) + elif space is not None: + dataset_id = None + space_id = _find_space_id(api=self._spaces_api, space=space) + else: + raise ValueError( + "Either 'dataset' or 'space' must be provided: 'dataset' to " + "create an experiment associated with a dataset, or 'space' " + "to create a standalone experiment." + ) + if not isinstance(experiment_runs, list | pd.DataFrame): raise TypeError( "Experiment runs must be a list of dicts or a pandas DataFrame" @@ -207,7 +251,8 @@ def create( get_payload_size_mb(experiment_runs) <= self._sdk_config.max_http_payload_size_mb ) - if below_threshold or force_http: + + if dataset_id is None or below_threshold or force_http: from arize._generated import api_client as gen data = experiment_df.to_dict(orient="records") @@ -224,6 +269,7 @@ def create( body = gen.CreateExperimentRequest( name=name, dataset_id=dataset_id, + space_id=space_id, experiment_runs=runs_create, ) return self._api.create_experiment(create_experiment_request=body) @@ -268,7 +314,10 @@ def get( Args: experiment: Experiment name or ID to retrieve. dataset: Optional dataset name or ID used to resolve ``experiment`` by name. - space: Optional space name or ID used to resolve ``dataset`` by name. + space: Optional space name or ID. Resolves ``dataset`` when that is a + name, and — when ``dataset`` is not provided — resolves + ``experiment`` by name directly within the space, which is the + only option for an experiment with no dataset. Returns: The experiment object. @@ -280,6 +329,7 @@ def get( experiment_id = _find_experiment_id( api=self._api, datasets_api=self._datasets_api, + spaces_api=self._spaces_api, experiment=experiment, dataset=dataset, space=space, @@ -313,6 +363,7 @@ def delete( experiment_id = _find_experiment_id( api=self._api, datasets_api=self._datasets_api, + spaces_api=self._spaces_api, experiment=experiment, dataset=dataset, space=space, @@ -347,8 +398,11 @@ def list_runs( Args: experiment: Experiment name or ID to list runs for. - dataset: Optional dataset name or ID used to resolve ``experiment`` by name. - space: Optional space name or ID used to resolve ``dataset`` by name. + dataset: Optional dataset name or ID used to resolve ``experiment`` + by name, for an experiment associated with a dataset. + space: Optional space name or ID. Used to resolve ``dataset`` by + name, or — when ``dataset`` is not provided — to resolve a + experiment not associated with a dataset's ``experiment`` name directly. limit: Maximum number of runs to return when ``all=False``. The server enforces an upper bound (500). cursor: Opaque pagination cursor from a previous response's @@ -361,8 +415,6 @@ def list_runs( A response object containing ``experiment_runs`` and ``pagination`` metadata. Raises: - ValueError: If ``all=True`` and the experiment has no associated - dataset. RuntimeError: If the Flight request fails or returns no response when ``all=True``. ApiException: If the REST API @@ -371,6 +423,7 @@ def list_runs( experiment_id = _find_experiment_id( api=self._api, datasets_api=self._datasets_api, + spaces_api=self._spaces_api, experiment=experiment, dataset=dataset, space=space, @@ -393,22 +446,8 @@ def _list_all_experiment_runs( when caching is enabled. """ experiment_obj = self.get(experiment=experiment_id) - # The Flight path needs the experiment's space_id, currently derived via - # its dataset, so a dataset-less experiment can't use it. The paginated - # REST path (all=False) has no such dependency and is unaffected. - if experiment_obj.dataset_id is None: - raise ValueError( - f"Experiment {experiment_id!r} has no associated dataset; " - "list_runs(all=True) is not supported for experiments " - "without a dataset." - ) experiment_updated_at = getattr(experiment_obj, "updated_at", None) - # TODO(Kiko): Space ID should not be needed, - # should work on server tech debt to remove this - dataset_obj = self._datasets_api.get_dataset( - dataset_id=experiment_obj.dataset_id - ) - space_id = dataset_obj.space_id + space_id = experiment_obj.space_id experiment_df = None # try to load dataset from cache @@ -498,8 +537,9 @@ def append_runs( Payload requirements (server-enforced): - Provide between 1 and 1000 runs per request. - - Each run must include ``example_id`` (ID of an example from - the experiment's dataset) and ``output``. + - Each run must include ``output``. ``example_id`` (ID of an example + from the experiment's dataset) is required only when the target + experiment is associated with a dataset. - Additional user-defined fields (e.g. ``model``, ``latency_ms``) are allowed per run. @@ -507,7 +547,9 @@ def append_runs( experiment: Experiment ID or name to append runs to. dataset: Optional dataset name or ID used to resolve ``experiment`` by name. - space: Optional space name or ID used to resolve ``dataset`` by name. + space: Optional space name or ID. Resolves ``dataset`` when that is a + name, and — when ``dataset`` is not provided — resolves + ``experiment`` by name directly within the space. experiment_runs: Runs to append, provided as either: - a list of JSON-like dicts, or - a :class:`pandas.DataFrame` (converted to records before upload). @@ -525,6 +567,7 @@ def append_runs( experiment_id = _find_experiment_id( api=self._api, datasets_api=self._datasets_api, + spaces_api=self._spaces_api, experiment=experiment, dataset=dataset, space=space, @@ -591,6 +634,7 @@ def annotate_runs( experiment_id = _find_experiment_id( api=self._api, datasets_api=self._datasets_api, + spaces_api=self._spaces_api, experiment=experiment, dataset=dataset, space=space, diff --git a/src/arize/experiments/functions.py b/src/arize/experiments/functions.py index f81c37d..32394a2 100644 --- a/src/arize/experiments/functions.py +++ b/src/arize/experiments/functions.py @@ -902,17 +902,22 @@ def transform_to_experiment_format( if isinstance(experiment_runs, pd.DataFrame) else pd.DataFrame(experiment_runs) ) - # Validate required columns - required_cols = {task_fields.example_id, task_fields.output} + # Validate required columns. example_id links a run to a dataset example, + # so it's only required when task_fields identifies one (i.e. the + # experiment is associated with a dataset). + required_cols = {task_fields.output} + if task_fields.example_id is not None: + required_cols.add(task_fields.example_id) missing_cols = required_cols - set(data.columns) if missing_cols: raise ValueError(f"Missing required columns: {missing_cols}") # Initialize output DataFrame with required columns out_df = data.copy() - out_df["example_id"] = data[task_fields.example_id] - if task_fields.example_id != "example_id": - out_df.drop(task_fields.example_id, axis=1, inplace=True) + if task_fields.example_id is not None: + out_df["example_id"] = data[task_fields.example_id] + if task_fields.example_id != "example_id": + out_df.drop(task_fields.example_id, axis=1, inplace=True) out_df["output"] = data[task_fields.output].apply( lambda x: json.dumps(x) if isinstance(x, dict) else x ) diff --git a/src/arize/experiments/types.py b/src/arize/experiments/types.py index 090619d..fe9d1a0 100644 --- a/src/arize/experiments/types.py +++ b/src/arize/experiments/types.py @@ -413,13 +413,14 @@ class ExperimentTaskFieldNames: """Column names for mapping experiment task results in a :class:`pandas.DataFrame`. Args: - example_id: Name of column containing example IDs. - The ID values must match the id of the dataset rows. + example_id: Name of column containing example IDs. The ID values must + match the id of the dataset rows. Required when creating an + experiment associated with a dataset. output: Name of column containing task results """ - example_id: str output: str + example_id: str | None = None TaskOutput = JSONSerializable diff --git a/src/arize/integrations/__init__.py b/src/arize/integrations/__init__.py new file mode 100644 index 0000000..51f46fd --- /dev/null +++ b/src/arize/integrations/__init__.py @@ -0,0 +1 @@ +"""Integration management for the Arize platform (LLM + agent integrations).""" diff --git a/src/arize/integrations/client.py b/src/arize/integrations/client.py new file mode 100644 index 0000000..7300083 --- /dev/null +++ b/src/arize/integrations/client.py @@ -0,0 +1,558 @@ +"""Client implementation for managing integrations in the Arize platform.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from arize._utils import unwrap_oneof +from arize.constants.config import DEFAULT_LIST_LIMIT +from arize.integrations.types import ( + CreateAnthropicConfig, + CreateAwsBedrockConfig, + CreateCustomConfig, + CreateGeminiConfig, + CreateLlmConfig, + CreateNvidiaNimConfig, + CreateOpenAiConfig, + CreateVertexAiConfig, + IntegrationType, + ListIntegrationsResponse, +) +from arize.pre_releases import ReleaseStage, prerelease_endpoint +from arize.utils.resolve import ( + _find_integration_id, + _resolve_resource, +) +from arize.utils.unset import _UNSET, UNSET, is_provided + +if TYPE_CHECKING: + # builtins is needed to use builtins.list in type annotations because + # the class has a list() method that shadows the built-in list type + import builtins + + from arize._generated.api_client.api_client import ApiClient + from arize.config import SDKConfiguration + from arize.integrations.types import ( + AgentIntegration, + CreateAgentRequestPresetInput, + CreateAwsBedrockAuth, + IntegrationScoping, + LlmIntegration, + UpdateAgentRequestPresetInput, + ) + +# The provider-discriminated config accepted by ``create_llm``. Callers +# construct the generated per-provider config for the provider they want +# (all 7 are supported); a pre-wrapped ``CreateLlmConfig`` is also accepted. +# Declared at runtime (not under TYPE_CHECKING) so downstream consumers such as +# the ax CLI can import and reference it. +CreateLlmConfigInput = ( + CreateOpenAiConfig + | CreateAnthropicConfig + | CreateGeminiConfig + | CreateAwsBedrockConfig + | CreateCustomConfig + | CreateVertexAiConfig + | CreateNvidiaNimConfig + | CreateLlmConfig +) + +logger = logging.getLogger(__name__) + + +class IntegrationsClient: + """Client for managing Arize integrations (LLM and agent). + + This class is primarily intended for internal use within the SDK. Users are + highly encouraged to access resource-specific functionality via + :class:`arize.ArizeClient`. + + The integrations client is a thin wrapper around the generated REST API + client, using the shared generated API client owned by + :class:`arize.config.SDKConfiguration`. + + Integrations are polymorphic: ``LLM`` integrations configure a model + provider (``OPEN_AI``, ``ANTHROPIC``, ``GEMINI``, ``AWS_BEDROCK``, + ``CUSTOM``, ``VERTEX_AI``, or ``NVIDIA_NIM``), while ``AGENT`` integrations + connect a customer-hosted agent exposed at an HTTP endpoint. The integration + :class:`~arize.integrations.types.IntegrationType` selects the config shape. + """ + + def __init__( + self, *, sdk_config: SDKConfiguration, generated_client: ApiClient + ) -> None: + """ + Args: + sdk_config: Resolved SDK configuration. + generated_client: Shared generated API client instance. + """ # noqa: D205, D212 + self._sdk_config = sdk_config + + # Import at runtime to keep the module lazy-loaded + from arize._generated import api_client as gen + + self._api = gen.IntegrationsApi(generated_client) + + @prerelease_endpoint(key="integrations.list", stage=ReleaseStage.ALPHA) + def list( + self, + *, + integration_type: IntegrationType | None = None, + name: str | None = None, + space: str | None = None, + limit: int = DEFAULT_LIST_LIMIT, + cursor: str | None = None, + ) -> ListIntegrationsResponse: + """List integrations the user has access to. + + When *integration_type* is omitted, integrations of every type are + returned in one merged list; each item carries its type. Integrations + are returned in descending creation order (most recently created + first). + + Args: + integration_type: Optional filter to a single integration type + (:attr:`~arize.integrations.types.IntegrationType.LLM` or + :attr:`~arize.integrations.types.IntegrationType.AGENT`). + name: Optional case-insensitive substring filter on the integration name. + space: Optional space filter. If the value is a base64-encoded resource ID it is + treated as a space ID; otherwise it is used as a case-insensitive + substring filter on the space name. + limit: Maximum number of integrations to return. The server may enforce + an upper bound (max 100). + cursor: Opaque pagination cursor from a previous response. + + Returns: + A response object with the integrations and pagination information. + + Raises: + ApiException: If the REST API + returns an error response (e.g. 400/401/403/429). + """ + resolved_space = _resolve_resource(space) + result = self._api.list_integrations( + type=integration_type, + space_id=resolved_space.id, + space_name=resolved_space.name, + name=name, + limit=limit, + cursor=cursor, + ) + return ListIntegrationsResponse.model_validate( + result, from_attributes=True + ) + + @prerelease_endpoint(key="integrations.get", stage=ReleaseStage.ALPHA) + def get( + self, + *, + integration: str, + integration_type: IntegrationType | None = None, + space: str | None = None, + ) -> AgentIntegration | LlmIntegration: + """Get an integration by ID or name. + + Args: + integration: Integration ID or name. If a name is provided, + *integration_type* is used to resolve it (and *space* if given). + integration_type: The integration type used to resolve + *integration* by name. Names are only unique per + ``(account, type)``, so this is required when *integration* is + a name; it is ignored when *integration* is an ID. + space: Optional space ID or name. This is only a visibility + filter, not required to resolve a name. + + Returns: + The concrete integration object (:class:`AgentIntegration` or + :class:`LlmIntegration`). + + Raises: + NotFoundError: If *integration* is a name and *integration_type* + is not provided, or the name cannot be resolved. + ApiException: If the REST API + returns an error response (e.g. 401/403/404/429). + """ + integration_id = _find_integration_id( + api=self._api, + integration=integration, + integration_type=integration_type, + space=space, + ) + result = self._api.get_integration(integration_id=integration_id) + return unwrap_oneof(result) # type: ignore[return-value] + + @prerelease_endpoint(key="integrations.create", stage=ReleaseStage.ALPHA) + def create_llm( + self, + *, + name: str, + config: CreateLlmConfigInput, + scopings: builtins.list[IntegrationScoping] | None = None, + ) -> LlmIntegration: + """Create an LLM integration. + + LLM integrations configure access to a model provider for use within + the Arize platform. All 7 providers are supported; construct the + matching generated config for the ``config`` argument: + + - ``OPEN_AI`` — :class:`~arize.integrations.types.CreateOpenAiConfig` + - ``ANTHROPIC`` — :class:`~arize.integrations.types.CreateAnthropicConfig` + - ``GEMINI`` — :class:`~arize.integrations.types.CreateGeminiConfig` + - ``AWS_BEDROCK`` — :class:`~arize.integrations.types.CreateAwsBedrockConfig` + (nests a :class:`~arize.integrations.types.CreateAwsBedrockAuth`: + DEFAULT, BEARER_TOKEN, or PROXY_WITH_HEADERS) + - ``CUSTOM`` — :class:`~arize.integrations.types.CreateCustomConfig` + - ``VERTEX_AI`` — :class:`~arize.integrations.types.CreateVertexAiConfig` + - ``NVIDIA_NIM`` — :class:`~arize.integrations.types.CreateNvidiaNimConfig` + + Integration names must be unique within the account for the ``LLM`` type. + + Args: + name: Integration name (must be unique within the account per type). + config: The provider-specific config. Accepts any of the 7 generated + per-provider ``Create*Config`` objects, or a pre-wrapped + :class:`~arize.integrations.types.CreateLlmConfig` union. + scopings: Visibility scoping rules. Defaults to account-wide if omitted. + + Returns: + The created LLM integration. + + Raises: + ApiException: If the REST API + returns an error response (e.g. 400/401/403/409/422/429). + """ + from arize._generated import api_client as gen + + llm_config = ( + config + if isinstance(config, gen.CreateLlmConfig) + else gen.CreateLlmConfig(actual_instance=config) + ) + body = gen.CreateIntegrationRequest( + actual_instance=gen.CreateLlmIntegrationRequest( + type=IntegrationType.LLM.value, + name=name, + scopings=scopings, + config=llm_config, + ) + ) + result = self._api.create_integration(create_integration_request=body) + return unwrap_oneof(result) # type: ignore[return-value] + + @prerelease_endpoint(key="integrations.create", stage=ReleaseStage.ALPHA) + def create_agent( + self, + *, + name: str, + endpoint: str, + input_schema: dict[str, Any], + description: str | None = None, + headers: dict[str, str] | None = None, + request_presets: builtins.list[CreateAgentRequestPresetInput] + | None = None, + scopings: builtins.list[IntegrationScoping] | None = None, + ) -> AgentIntegration: + """Create an agent integration. + + Agent integrations connect a customer-hosted agent exposed at an HTTPS + endpoint. Integration names must be unique within the account for the + ``AGENT`` type. + + Args: + name: Integration name (must be unique within the account per type). + endpoint: HTTPS endpoint URL Arize calls for replay. Validated + server-side for SSRF (must resolve to a public address). + input_schema: JSON Schema (Draft-07) the endpoint's request body + conforms to. + description: Optional human-readable description of the integration. + headers: Optional custom headers to include in requests. Encrypted + at rest and never returned in responses. + request_presets: Optional initial named request presets. + scopings: Visibility scoping rules. Defaults to account-wide if omitted. + + Returns: + The created agent integration. + + Raises: + ApiException: If the REST API + returns an error response (e.g. 400/401/403/409/422/429). + """ + from arize._generated import api_client as gen + + body = gen.CreateIntegrationRequest( + actual_instance=gen.CreateAgentIntegrationRequest( + type=IntegrationType.AGENT.value, + name=name, + description=description, + scopings=scopings, + config=gen.CreateAgentConfig( + endpoint=endpoint, + input_schema=input_schema, + headers=headers, + request_presets=request_presets, + ), + ) + ) + result = self._api.create_integration(create_integration_request=body) + return unwrap_oneof(result) # type: ignore[return-value] + + @prerelease_endpoint(key="integrations.update", stage=ReleaseStage.ALPHA) + def update_llm( + self, + *, + integration: str, + space: str | None = None, + name: str | UNSET = _UNSET, + api_key: str | None | UNSET = _UNSET, + function_calling_enabled: bool | UNSET = _UNSET, + auth: CreateAwsBedrockAuth | UNSET = _UNSET, + base_url: str | None | UNSET = _UNSET, + headers: dict[str, str] | None | UNSET = _UNSET, + is_default_models_enabled: bool | UNSET = _UNSET, + model_names: builtins.list[str] | UNSET = _UNSET, + project_id: str | UNSET = _UNSET, + location: str | UNSET = _UNSET, + project_access_label: str | UNSET = _UNSET, + scopings: builtins.list[IntegrationScoping] | UNSET = _UNSET, + ) -> LlmIntegration: + """Update an LLM integration by ID or name. + + At least one updatable field must be provided; otherwise a + ``ValueError`` is raised (the server rejects type-only PATCHes). + Only the fields you pass are sent to the server; omitted fields are + left unchanged. The provider is immutable. The config fields map to the + flat ``UpdateLlmConfig`` and are provider-conditional; the server + rejects fields that do not apply to the stored provider with a 422, so + pass only the fields valid for that provider: + + - ``api_key``, ``function_calling_enabled`` — all providers except + ``AWS_BEDROCK`` and ``VERTEX_AI``. + - ``auth`` — ``AWS_BEDROCK`` only; replaces the stored auth wholesale. + - ``base_url``, ``headers`` — ``CUSTOM`` and ``NVIDIA_NIM`` only. + - ``is_default_models_enabled``, ``model_names`` — ``AWS_BEDROCK``, + ``CUSTOM``, and ``NVIDIA_NIM`` only. + - ``project_id``, ``location``, ``project_access_label`` — + ``VERTEX_AI`` only. + + Nullable fields (``api_key``, ``base_url``, ``headers``) accept an + explicit ``None`` to clear them (omit to keep unchanged). + + Args: + integration: Integration ID or name. If a name is provided, it is + resolved using the ``LLM`` type (and *space* if given). + space: Optional space ID or name. Integration names are unique per + ``(account, type)``, so this is only a visibility filter, not + required to resolve a name. + name: New integration name. Must be unique within the account per type. + api_key: New API key. Pass ``None`` to clear the existing key. + function_calling_enabled: Updated function calling flag. + auth: Replacement AWS Bedrock auth + (:class:`~arize.integrations.types.CreateAwsBedrockAuth`). + base_url: New endpoint URL. Pass ``None`` to clear (``NVIDIA_NIM``; + ``CUSTOM`` rejects ``None`` with a 422). + headers: Replacement custom request headers as a name-to-value map. + Pass ``None`` to clear all headers. + is_default_models_enabled: Toggle Arize's default model catalog. + model_names: Replacement custom model list. + project_id: New Vertex AI GCP project ID. + location: New Vertex AI GCP region. + project_access_label: New Vertex AI project-access label. + scopings: Replacement visibility scoping rules (replaces all existing). + + Returns: + The updated LLM integration. + + Raises: + ValueError: If no updatable field is provided. + ApiException: If the REST API + returns an error response (e.g. 400/401/403/404/409/422/429). + """ + from arize._generated import api_client as gen + + config_kwargs: dict[str, Any] = { + k: v + for k, v in ( + ("api_key", api_key), + ("is_function_calling_enabled", function_calling_enabled), + ("auth", auth), + ("base_url", base_url), + ("headers", headers), + ("is_default_models_enabled", is_default_models_enabled), + ("model_names", model_names), + ("project_id", project_id), + ("location", location), + ("project_access_label", project_access_label), + ) + if is_provided(v) + } + envelope_kwargs: dict[str, Any] = {"type": IntegrationType.LLM.value} + if is_provided(name): + envelope_kwargs["name"] = name + if is_provided(scopings): + envelope_kwargs["scopings"] = scopings + if config_kwargs: + envelope_kwargs["config"] = gen.UpdateLlmConfig(**config_kwargs) + + # The API rejects type-only PATCHes; reject empty updates locally so + # callers get a clear error instead of an opaque 422. + if ( + not is_provided(name) + and not is_provided(scopings) + and not config_kwargs + ): + raise ValueError( + "At least one field must be provided to update the " + "integration (name, scopings, or a config field)." + ) + + integration_id = _find_integration_id( + api=self._api, + integration=integration, + integration_type=IntegrationType.LLM, + space=space, + ) + body = gen.UpdateIntegrationRequest( + actual_instance=gen.UpdateLlmIntegrationRequest(**envelope_kwargs) + ) + result = self._api.update_integration( + integration_id=integration_id, + update_integration_request=body, + ) + return unwrap_oneof(result) # type: ignore[return-value] + + @prerelease_endpoint(key="integrations.update", stage=ReleaseStage.ALPHA) + def update_agent( + self, + *, + integration: str, + space: str | None = None, + name: str | UNSET = _UNSET, + description: str | None | UNSET = _UNSET, + endpoint: str | UNSET = _UNSET, + input_schema: dict[str, Any] | UNSET = _UNSET, + headers: dict[str, str] | None | UNSET = _UNSET, + request_presets: builtins.list[UpdateAgentRequestPresetInput] + | UNSET = _UNSET, + scopings: builtins.list[IntegrationScoping] | UNSET = _UNSET, + ) -> AgentIntegration: + """Update an agent integration by ID or name. + + At least one updatable field must be provided; otherwise a + ``ValueError`` is raised (the server rejects type-only PATCHes). + Only the fields you pass are sent to the server; omitted fields are + left unchanged. Collection fields (``headers``, ``request_presets``, + ``scopings``) replace the existing values when provided. To clear + nullable fields (``description``, ``headers``), pass ``None``. + + Args: + integration: Integration ID or name. If a name is provided, it is + resolved using the ``AGENT`` type (and *space* if given). + space: Optional space ID or name. Integration names are unique per + ``(account, type)``, so this is only a visibility filter, not + required to resolve a name. + name: New integration name. Must be unique within the account per type. + description: New description. Pass ``None`` to clear. + endpoint: New HTTPS endpoint URL. + input_schema: New JSON Schema for the request payload shape. + headers: Replacement custom headers. Pass ``None`` (or ``{}``) to + clear all headers. + request_presets: Replacement request presets, matched by ``name``. + scopings: Replacement visibility scoping rules (replaces all existing). + + Returns: + The updated agent integration. + + Raises: + ValueError: If no updatable field is provided. + ApiException: If the REST API + returns an error response (e.g. 400/401/403/404/409/422/429). + """ + from arize._generated import api_client as gen + + config_kwargs: dict[str, Any] = { + k: v + for k, v in ( + ("endpoint", endpoint), + ("input_schema", input_schema), + ("headers", headers), + ("request_presets", request_presets), + ) + if is_provided(v) + } + envelope_kwargs: dict[str, Any] = {"type": IntegrationType.AGENT.value} + if is_provided(name): + envelope_kwargs["name"] = name + if is_provided(description): + envelope_kwargs["description"] = description + if is_provided(scopings): + envelope_kwargs["scopings"] = scopings + if config_kwargs: + envelope_kwargs["config"] = gen.UpdateAgentConfig(**config_kwargs) + + # The API rejects type-only PATCHes; reject empty updates locally so + # callers get a clear error instead of an opaque 422. + if ( + not is_provided(name) + and not is_provided(description) + and not is_provided(scopings) + and not config_kwargs + ): + raise ValueError( + "At least one field must be provided to update the " + "integration (name, description, scopings, or a config field)." + ) + + integration_id = _find_integration_id( + api=self._api, + integration=integration, + integration_type=IntegrationType.AGENT, + space=space, + ) + body = gen.UpdateIntegrationRequest( + actual_instance=gen.UpdateAgentIntegrationRequest(**envelope_kwargs) + ) + result = self._api.update_integration( + integration_id=integration_id, + update_integration_request=body, + ) + return unwrap_oneof(result) # type: ignore[return-value] + + @prerelease_endpoint(key="integrations.delete", stage=ReleaseStage.ALPHA) + def delete( + self, + *, + integration: str, + integration_type: IntegrationType | None = None, + space: str | None = None, + ) -> None: + """Delete an integration by ID or name. + + This operation is irreversible. + + Args: + integration: Integration ID or name. If a name is provided, + *integration_type* is used to resolve it (and *space* if given). + integration_type: The integration type used to resolve + *integration* by name. Names are only unique per + ``(account, type)``, so this is required when *integration* is + a name; it is ignored when *integration* is an ID. + space: Optional space ID or name. This is only a visibility + filter, not required to resolve a name. + + Returns: + This method returns None on success (common empty 204 response). + + Raises: + NotFoundError: If *integration* is a name and *integration_type* + is not provided, or the name cannot be resolved. + ApiException: If the REST API + returns an error response (e.g. 401/403/404/429). + """ + integration_id = _find_integration_id( + api=self._api, + integration=integration, + integration_type=integration_type, + space=space, + ) + self._api.delete_integration(integration_id=integration_id) diff --git a/src/arize/integrations/types.py b/src/arize/integrations/types.py new file mode 100644 index 0000000..525dba2 --- /dev/null +++ b/src/arize/integrations/types.py @@ -0,0 +1,154 @@ +"""Public types for the integrations subdomain.""" + +from pydantic import BaseModel, ConfigDict, field_validator + +from arize._generated.api_client.models.agent_config import AgentConfig +from arize._generated.api_client.models.agent_integration import ( + AgentIntegration, +) +from arize._generated.api_client.models.agent_request_preset import ( + AgentRequestPreset, +) +from arize._generated.api_client.models.anthropic_config import AnthropicConfig +from arize._generated.api_client.models.aws_bedrock_auth import AwsBedrockAuth +from arize._generated.api_client.models.aws_bedrock_bearer_token_auth import ( + AwsBedrockBearerTokenAuth, +) +from arize._generated.api_client.models.aws_bedrock_config import ( + AwsBedrockConfig, +) +from arize._generated.api_client.models.aws_bedrock_default_auth import ( + AwsBedrockDefaultAuth, +) +from arize._generated.api_client.models.aws_bedrock_proxy_with_headers_auth import ( + AwsBedrockProxyWithHeadersAuth, +) +from arize._generated.api_client.models.create_agent_request_preset_input import ( + CreateAgentRequestPresetInput, +) +from arize._generated.api_client.models.create_anthropic_config import ( + CreateAnthropicConfig, +) +from arize._generated.api_client.models.create_aws_bedrock_auth import ( + CreateAwsBedrockAuth, +) +from arize._generated.api_client.models.create_aws_bedrock_bearer_token_auth import ( + CreateAwsBedrockBearerTokenAuth, +) +from arize._generated.api_client.models.create_aws_bedrock_config import ( + CreateAwsBedrockConfig, +) +from arize._generated.api_client.models.create_aws_bedrock_default_auth import ( + CreateAwsBedrockDefaultAuth, +) +from arize._generated.api_client.models.create_aws_bedrock_proxy_with_headers_auth import ( + CreateAwsBedrockProxyWithHeadersAuth, +) +from arize._generated.api_client.models.create_custom_config import ( + CreateCustomConfig, +) +from arize._generated.api_client.models.create_gemini_config import ( + CreateGeminiConfig, +) +from arize._generated.api_client.models.create_llm_config import CreateLlmConfig +from arize._generated.api_client.models.create_nvidia_nim_config import ( + CreateNvidiaNimConfig, +) +from arize._generated.api_client.models.create_open_ai_config import ( + CreateOpenAiConfig, +) +from arize._generated.api_client.models.create_vertex_ai_config import ( + CreateVertexAiConfig, +) +from arize._generated.api_client.models.custom_config import CustomConfig +from arize._generated.api_client.models.gemini_config import GeminiConfig +from arize._generated.api_client.models.integration import Integration +from arize._generated.api_client.models.integration_scoping import ( + IntegrationScoping, +) +from arize._generated.api_client.models.integration_type import IntegrationType +from arize._generated.api_client.models.llm_config import LlmConfig +from arize._generated.api_client.models.llm_integration import LlmIntegration +from arize._generated.api_client.models.llm_integration_provider import ( + LlmIntegrationProvider, +) +from arize._generated.api_client.models.nvidia_nim_config import NvidiaNimConfig +from arize._generated.api_client.models.open_ai_config import OpenAiConfig +from arize._generated.api_client.models.pagination_metadata import ( + PaginationMetadata, +) +from arize._generated.api_client.models.update_agent_request_preset_input import ( + UpdateAgentRequestPresetInput, +) +from arize._generated.api_client.models.update_llm_config import UpdateLlmConfig +from arize._generated.api_client.models.vertex_ai_config import VertexAiConfig + + +class ListIntegrationsResponse(BaseModel): + """SDK view of the generated list response with each ``Integration`` unwrapped. + + The ``integrations`` field contains the concrete inner types + (:class:`AgentIntegration` or :class:`LlmIntegration`) instead of the + oneOf wrapper :class:`Integration`. + """ + + integrations: list[AgentIntegration | LlmIntegration] + pagination: PaginationMetadata + + model_config = ConfigDict(from_attributes=True) + + @field_validator("integrations", mode="before") + @classmethod + def _coerce_integrations( + cls, v: object + ) -> list[AgentIntegration | LlmIntegration]: + result = [] + for item in v: # type: ignore[attr-defined] + if isinstance(item, Integration): + if item.actual_instance is None: + raise ValueError( + "Integration wrapper has actual_instance=None" + ) + item = item.actual_instance + result.append(item) + return result + + +__all__ = [ + "AgentConfig", + "AgentIntegration", + "AgentRequestPreset", + "AnthropicConfig", + "AwsBedrockAuth", + "AwsBedrockBearerTokenAuth", + "AwsBedrockConfig", + "AwsBedrockDefaultAuth", + "AwsBedrockProxyWithHeadersAuth", + "CreateAgentRequestPresetInput", + "CreateAnthropicConfig", + "CreateAwsBedrockAuth", + "CreateAwsBedrockBearerTokenAuth", + "CreateAwsBedrockConfig", + "CreateAwsBedrockDefaultAuth", + "CreateAwsBedrockProxyWithHeadersAuth", + "CreateCustomConfig", + "CreateGeminiConfig", + "CreateLlmConfig", + "CreateNvidiaNimConfig", + "CreateOpenAiConfig", + "CreateVertexAiConfig", + "CustomConfig", + "GeminiConfig", + "IntegrationScoping", + "IntegrationType", + "ListIntegrationsResponse", + "LlmConfig", + "LlmIntegration", + "LlmIntegrationProvider", + "NvidiaNimConfig", + "OpenAiConfig", + "PaginationMetadata", + "UpdateAgentRequestPresetInput", + "UpdateLlmConfig", + "VertexAiConfig", +] diff --git a/src/arize/organizations/client.py b/src/arize/organizations/client.py index 2f5cc92..cbc56f2 100644 --- a/src/arize/organizations/client.py +++ b/src/arize/organizations/client.py @@ -3,12 +3,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from arize.constants.config import DEFAULT_LIST_LIMIT from arize.organizations.types import OrganizationMembership from arize.pre_releases import ReleaseStage, prerelease_endpoint from arize.utils.resolve import _find_organization_id +from arize.utils.unset import _UNSET, UNSET, is_provided if TYPE_CHECKING: from arize._generated.api_client.api_client import ApiClient @@ -160,15 +161,18 @@ def update( *, organization: str, name: str | None = None, - description: str | None = None, + description: str | None | UNSET = _UNSET, ) -> Organization: """Update an organization's metadata by ID or name. + Only fields you pass are sent to the server. Omitted fields are left + unchanged; pass ``None`` for ``description`` to clear it. + Args: organization: Organization ID or name to update. name: Updated name for the organization. - description: Updated description for the organization. Pass an - empty string to clear the existing description. + description: Updated description for the organization. Pass + ``None`` to clear the existing description. Returns: The updated organization object. @@ -178,7 +182,13 @@ def update( ApiException: If the API request fails (for example, organization not found or insufficient permissions). """ - if name is None and description is None: + kwargs: dict[str, Any] = {} + if name is not None: + kwargs["name"] = name + if is_provided(description): + kwargs["description"] = description + + if not kwargs: raise ValueError( "At least one of 'name' or 'description' must be provided" ) @@ -187,10 +197,7 @@ def update( from arize._generated import api_client as gen - body = gen.UpdateOrganizationRequest( - name=name, - description=description, - ) + body = gen.UpdateOrganizationRequest(**kwargs) return self._api.update_organization( org_id=org_id, update_organization_request=body ) diff --git a/src/arize/prompts/client.py b/src/arize/prompts/client.py index 289812b..69f03b3 100644 --- a/src/arize/prompts/client.py +++ b/src/arize/prompts/client.py @@ -225,7 +225,7 @@ def update( *, prompt: str, space: str | None = None, - description: str, + description: str | None, ) -> Prompt: """Update a prompt's metadata. @@ -233,13 +233,13 @@ def update( prompt: Prompt ID or name. If a name is provided, ``space`` must also be supplied so the name can be resolved. space: Optional space ID or name. Required when *prompt* is a name. - description: Updated description for the prompt. + description: Updated description for the prompt. Pass ``None`` to + clear it. Returns: The updated prompt object. Raises: - ValueError: If no fields to update are provided. ApiException: If the REST API returns an error response (e.g. 401/403/404/429). """ diff --git a/src/arize/roles/client.py b/src/arize/roles/client.py index 84fa164..6e81fb2 100644 --- a/src/arize/roles/client.py +++ b/src/arize/roles/client.py @@ -3,11 +3,12 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from arize.constants.config import DEFAULT_LIST_LIMIT from arize.pre_releases import ReleaseStage, prerelease_endpoint from arize.utils.resolve import _find_role_id +from arize.utils.unset import _UNSET, UNSET, is_provided if TYPE_CHECKING: # builtins is needed for builtins.list in annotations because this class @@ -141,23 +142,25 @@ def update( self, *, role: str, - name: str | None = None, - description: str | None = None, - permissions: builtins.list[Permission] | None = None, + name: str | None | UNSET = _UNSET, + description: str | None | UNSET = _UNSET, + permissions: builtins.list[Permission] | None | UNSET = _UNSET, ) -> Role: """Update a custom role by name or ID. - At least one field must be provided. Predefined roles cannot be updated. - When ``permissions`` is provided, the existing permissions are fully - replaced with the new set. + At least one field must be provided. Omitted fields are preserved. + Passing ``description=None`` clears the description. Predefined roles + cannot be updated. When ``permissions`` is provided, the existing + permissions are fully replaced with the new set. Args: role: Role name or identifier (base64). If the value looks like an ID it is used directly; otherwise it is resolved by name. - name: Updated name for the role (max 255 chars). - description: Updated description of the role (max 1000 chars). + name: Updated name for the role (max 255 chars). Omit to preserve. + description: Updated description of the role (max 1000 chars). Omit to + preserve, or pass ``None`` to clear. permissions: Replacement set of permissions. When provided, fully - replaces existing permissions. + replaces existing permissions. Omit or pass ``None`` to preserve. Returns: The updated role object. @@ -170,7 +173,15 @@ def update( (for example, role not found, insufficient permissions, or attempting to update a predefined role). """ - if name is None and description is None and permissions is None: + request_kwargs: dict[str, Any] = {} + if is_provided(name) and name is not None: + request_kwargs["name"] = name + if is_provided(description): + request_kwargs["description"] = description + if is_provided(permissions) and permissions is not None: + request_kwargs["permissions"] = permissions + + if not request_kwargs: raise ValueError( "At least one of 'name', 'description', or 'permissions' must be provided" ) @@ -179,11 +190,7 @@ def update( from arize._generated import api_client as gen - body = gen.UpdateRoleRequest( - name=name, - description=description, - permissions=permissions, - ) + body = gen.UpdateRoleRequest(**request_kwargs) return self._api.update_role(role_id=role_id, update_role_request=body) @prerelease_endpoint(key="roles.delete", stage=ReleaseStage.BETA) diff --git a/src/arize/spaces/client.py b/src/arize/spaces/client.py index d28d56a..e83fbb7 100644 --- a/src/arize/spaces/client.py +++ b/src/arize/spaces/client.py @@ -3,12 +3,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from arize.constants.config import DEFAULT_LIST_LIMIT from arize.pre_releases import ReleaseStage, prerelease_endpoint from arize.spaces.types import SpaceMembership from arize.utils.resolve import _find_space_id +from arize.utils.unset import _UNSET, UNSET, is_provided if TYPE_CHECKING: from arize._generated.api_client.api_client import ApiClient @@ -174,16 +175,20 @@ def update( self, *, space: str, - name: str | None = None, - description: str | None = None, + name: str | None | UNSET = _UNSET, + description: str | None | UNSET = _UNSET, is_private: bool | None = None, ) -> Space: """Update a space by ID or name. + Only fields you pass are sent to the server. Omitted fields are left + unchanged; pass ``None`` for ``description`` to clear it. + Args: space: Space ID or name to update. name: Updated name for the space. - description: Updated description for the space. + description: Updated description for the space. Pass ``None`` to + clear the existing description. is_private: Updated visibility for the space. Set to ``True`` to make the space private (visible only to members and admins), or ``False`` to make it public. When ``None``, the existing @@ -198,7 +203,15 @@ def update( ApiException: If the API request fails (for example, space not found or insufficient permissions). """ - if name is None and description is None and is_private is None: + kwargs: dict[str, Any] = {} + if is_provided(name) and name is not None: + kwargs["name"] = name + if is_provided(description): + kwargs["description"] = description + if is_private is not None: + kwargs["is_private"] = is_private + + if not kwargs: raise ValueError( "At least one of 'name', 'description', or 'is_private' must be provided" ) @@ -214,11 +227,7 @@ def update( from arize._generated import api_client as gen - body = gen.UpdateSpaceRequest( - name=name, - description=description, - is_private=is_private, - ) + body = gen.UpdateSpaceRequest(**kwargs) return self._api.update_space( space_id=space_id, update_space_request=body ) diff --git a/src/arize/spans/client.py b/src/arize/spans/client.py index 94fef4c..49a827e 100644 --- a/src/arize/spans/client.py +++ b/src/arize/spans/client.py @@ -91,6 +91,8 @@ def delete( project: str, span_ids: builtins.list[str], space: str | None = None, + start_time: datetime | None = None, + end_time: datetime | None = None, ) -> DeleteSpansResponse: """Permanently delete spans by their IDs. @@ -103,6 +105,12 @@ def delete( span_ids: List of span IDs to delete. space: Optional space name or ID used to disambiguate the project lookup. Required when ``project`` is a name. + start_time: Scope the delete to spans starting at or after this + timestamp (inclusive). When omitted, the server searches the + full 2-year lookback window. + end_time: Scope the delete to spans starting before this timestamp + (exclusive). When omitted, the server searches up to the + current time. Returns: A ``DeleteSpansResponse`` with ``completed`` (``True`` if no retry @@ -131,6 +139,8 @@ def delete( body = gen.DeleteSpansRequest( project_id=project_id, span_ids=span_ids, + start_time=start_time, + end_time=end_time, ) return self._api.delete_spans(delete_spans_request=body) diff --git a/src/arize/tasks/client.py b/src/arize/tasks/client.py index 3a3c7be..3d4c121 100644 --- a/src/arize/tasks/client.py +++ b/src/arize/tasks/client.py @@ -12,6 +12,7 @@ from arize.constants.config import DEFAULT_LIST_LIMIT from arize.pre_releases import ReleaseStage, prerelease_endpoint from arize.tasks.types import ( + AgentCallRunConfig, ListTasksResponse, LlmGenerationRunConfig, RunStatus, @@ -25,6 +26,7 @@ _find_task_id, _resolve_resource, ) +from arize.utils.unset import _UNSET, UNSET, is_provided if TYPE_CHECKING: import builtins @@ -47,16 +49,6 @@ _DEFAULT_TIMEOUT = 600.0 # seconds -# Sentinel for TasksClient.update — omit field from PATCH body vs explicit values. -# Defined as a class (rather than `object()`) so method signatures can spell -# out `str | _Missing` instead of the looser `str | object`. -class _Missing: - """Sentinel type used to distinguish "omitted" from explicit ``None``.""" - - -_MISSING: Final[_Missing] = _Missing() - - class TasksClient: """Client for managing Arize tasks and task runs. @@ -93,6 +85,7 @@ def __init__( @staticmethod def _coerce_run_configuration( item: RunConfiguration + | AgentCallRunConfig | LlmGenerationRunConfig | TemplateEvaluationRunConfig | dict, @@ -101,22 +94,29 @@ def _coerce_run_configuration( Accepts: - An already-wrapped ``RunConfiguration`` (returned as-is). - - An unwrapped inner type (``LlmGenerationRunConfig`` or - ``TemplateEvaluationRunConfig``), which is wrapped automatically. + - An unwrapped inner type (``AgentCallRunConfig``, + ``LlmGenerationRunConfig``, or ``TemplateEvaluationRunConfig``), which + is wrapped automatically. - A plain ``dict`` whose keys match one of the inner schemas; parsed via ``RunConfiguration.from_dict``. """ if isinstance(item, RunConfiguration): return item if isinstance( - item, (LlmGenerationRunConfig, TemplateEvaluationRunConfig) + item, + ( + AgentCallRunConfig, + LlmGenerationRunConfig, + TemplateEvaluationRunConfig, + ), ): return RunConfiguration(item) if isinstance(item, dict): return RunConfiguration.from_dict(item) raise TypeError( - f"run_configuration must be RunConfiguration, LlmGenerationRunConfig, " - f"TemplateEvaluationRunConfig, or dict; got {type(item)!r}" + f"run_configuration must be RunConfiguration, AgentCallRunConfig, " + f"LlmGenerationRunConfig, TemplateEvaluationRunConfig, or dict; " + f"got {type(item)!r}" ) # ------------------------------------------------------------------------- @@ -227,6 +227,7 @@ def _create( task_type: TaskType, evaluators: builtins.list[TaskEvaluatorInput] | None = None, run_configuration: RunConfiguration + | AgentCallRunConfig | LlmGenerationRunConfig | TemplateEvaluationRunConfig | dict @@ -270,7 +271,8 @@ def _create( run_configuration: Experiment run configuration. Required for ``"RUN_EXPERIMENT"`` tasks; must be omitted for eval task types. Use - :class:`arize.tasks.types.LlmGenerationRunConfig` or + :class:`arize.tasks.types.AgentCallRunConfig`, + :class:`arize.tasks.types.LlmGenerationRunConfig`, or :class:`arize.tasks.types.TemplateEvaluationRunConfig` wrapped in :class:`arize.tasks.types.RunConfiguration`. @@ -475,6 +477,7 @@ def create_run_experiment_task( name: str, dataset: str, run_configuration: RunConfiguration + | AgentCallRunConfig | LlmGenerationRunConfig | TemplateEvaluationRunConfig | dict, @@ -495,7 +498,8 @@ def create_run_experiment_task( dataset: Dataset name or identifier (base64) to run the experiment against. run_configuration: Discriminated experiment configuration. Use - :class:`arize.tasks.types.LlmGenerationRunConfig` or + :class:`arize.tasks.types.AgentCallRunConfig`, + :class:`arize.tasks.types.LlmGenerationRunConfig`, or :class:`arize.tasks.types.TemplateEvaluationRunConfig` wrapped in :class:`arize.tasks.types.RunConfiguration`. space: Optional space name or ID used to resolve ``dataset`` @@ -521,18 +525,19 @@ def update( *, task: str, space: str | None = None, - name: str | _Missing = _MISSING, + name: str | UNSET = _UNSET, # Evaluation-task fields - sampling_rate: float | _Missing = _MISSING, - is_continuous: bool | _Missing = _MISSING, - query_filter: str | None | _Missing = _MISSING, - evaluators: builtins.list[TaskEvaluatorInput] | _Missing = _MISSING, + sampling_rate: float | UNSET = _UNSET, + is_continuous: bool | UNSET = _UNSET, + query_filter: str | None | UNSET = _UNSET, + evaluators: builtins.list[TaskEvaluatorInput] | UNSET = _UNSET, # run_experiment-task fields run_configuration: RunConfiguration + | AgentCallRunConfig | LlmGenerationRunConfig | TemplateEvaluationRunConfig | dict - | _Missing = _MISSING, + | UNSET = _UNSET, ) -> Task: """Update mutable fields on an existing task. @@ -602,7 +607,7 @@ def update( "query_filter": query_filter, "evaluators": evaluators, }.items() - if not isinstance(v, _Missing) + if is_provided(v) } if eval_only_supplied: raise ValueError( @@ -610,46 +615,44 @@ def update( f"{', '.join(eval_only_supplied)}. " "Only 'name' and 'run_configuration' may be updated.", ) - run_exp_payload: dict[str, Any] = {} - if not isinstance(name, _Missing): - run_exp_payload["name"] = name - if not isinstance(run_configuration, _Missing): - run_exp_payload["run_configuration"] = ( + run_exp_kwargs: dict[str, Any] = {} + if is_provided(name): + run_exp_kwargs["name"] = name + if not isinstance(run_configuration, UNSET): + run_exp_kwargs["run_configuration"] = ( self._coerce_run_configuration(run_configuration) ) - if not run_exp_payload: + if not run_exp_kwargs: raise ValueError( "At least one update field must be provided for " "run_experiment tasks (name or run_configuration).", ) - inner_run_exp = gen.UpdateRunExperimentTaskRequest( - **run_exp_payload - ) + inner_run_exp = gen.UpdateRunExperimentTaskRequest(**run_exp_kwargs) body = gen.UpdateTaskRequest(actual_instance=inner_run_exp) else: # Evaluation task. - if not isinstance(run_configuration, _Missing): + if not isinstance(run_configuration, UNSET): raise ValueError( "'run_configuration' is only valid for run_experiment tasks, " f"not '{task_obj.type}'.", ) - eval_payload: dict[str, Any] = {} - if not isinstance(name, _Missing): - eval_payload["name"] = name - if not isinstance(sampling_rate, _Missing): - eval_payload["sampling_rate"] = sampling_rate - if not isinstance(is_continuous, _Missing): - eval_payload["is_continuous"] = is_continuous - if not isinstance(query_filter, _Missing): - eval_payload["query_filter"] = query_filter - if not isinstance(evaluators, _Missing): - eval_payload["evaluators"] = evaluators - if not eval_payload: + eval_kwargs: dict[str, Any] = {} + if is_provided(name): + eval_kwargs["name"] = name + if is_provided(sampling_rate): + eval_kwargs["sampling_rate"] = sampling_rate + if is_provided(is_continuous): + eval_kwargs["is_continuous"] = is_continuous + if is_provided(query_filter): + eval_kwargs["query_filter"] = query_filter + if is_provided(evaluators): + eval_kwargs["evaluators"] = evaluators + if not eval_kwargs: raise ValueError( "At least one update field must be provided " "(name, sampling_rate, is_continuous, query_filter, or evaluators).", ) - inner = gen.UpdateEvaluationTaskRequest(**eval_payload) + inner = gen.UpdateEvaluationTaskRequest(**eval_kwargs) body = gen.UpdateTaskRequest(actual_instance=inner) result = self._api.update_task( diff --git a/src/arize/tasks/types.py b/src/arize/tasks/types.py index b27c9f7..1687b70 100644 --- a/src/arize/tasks/types.py +++ b/src/arize/tasks/types.py @@ -94,6 +94,7 @@ class ListTasksResponse(BaseModel): __all__ = [ + "AgentCallRunConfig", "ListTaskRunsResponse", "ListTasksResponse", "LlmGenerationRunConfig", diff --git a/src/arize/utils/resolve.py b/src/arize/utils/resolve.py index fdbaae4..2619319 100644 --- a/src/arize/utils/resolve.py +++ b/src/arize/utils/resolve.py @@ -17,6 +17,7 @@ DatasetsApi, EvaluatorsApi, ExperimentsApi, + IntegrationsApi, OrganizationsApi, ProjectsApi, PromptsApi, @@ -25,6 +26,9 @@ TasksApi, UsersApi, ) + from arize._generated.api_client.models.integration_type import ( + IntegrationType, + ) logger = logging.getLogger(__name__) @@ -265,6 +269,7 @@ def _find_dataset_id( def _find_experiment_id( api: ExperimentsApi, datasets_api: DatasetsApi, + spaces_api: SpacesApi, experiment: str, dataset: str | None, space: str | None, @@ -274,15 +279,26 @@ def _find_experiment_id( Args: api: ExperimentsApi instance. datasets_api: DatasetsApi instance, used to resolve a dataset name to an ID. + spaces_api: SpacesApi instance, used to resolve a space name to an ID + when resolving a standalone experiment by name. experiment: Experiment ID or name. - dataset: Dataset ID or name. Required when *experiment* is a name. - space: Space ID or name used to resolve *dataset* by name. + dataset: Dataset ID or name. Provide this to resolve a + dataset-associated experiment by name. + space: Space ID or name. When *dataset* is a name, used to resolve + it. When *dataset* is not provided, used to resolve a standalone + experiment by name directly within that space. Returns: The resolved experiment ID. Raises: NotFoundError: If the experiment name cannot be found. + AmbiguousNameError: If *dataset* is not provided and *experiment* + matches both a standalone experiment and a dataset-associated + experiment in *space* (their names are only guaranteed unique + within each of those two scopes separately, not jointly). Pass + the experiment ID, or *dataset* to select the dataset-associated + one, to disambiguate. """ if is_resource_id(experiment): return experiment @@ -290,51 +306,83 @@ def _find_experiment_id( resolved_dataset = _resolve_resource(dataset) resolved_space = _resolve_resource(space) - if not resolved_dataset.is_set(): + if not resolved_dataset.is_set() and not resolved_space.is_set(): raise NotFoundError( "experiment", experiment, hint=( - "Provide 'dataset' so the experiment name can be resolved, " - "or provide the experiment ID instead of the name." + "Provide 'dataset' (to resolve a dataset-associated " + "experiment) or 'space' (to resolve a standalone experiment) " + "so the experiment name can be resolved, or provide the " + "experiment ID instead of the name." ), ) - if resolved_dataset.is_name() and not resolved_space.is_set(): - raise NotFoundError( - "experiment", - experiment, - hint=( - "Provide 'space' so the dataset name can be resolved, " - "which is needed to resolve the experiment name. Alternatively, " - "you can provide the experiment ID, or the dataset ID instead of the name." - ), + dataset_id: str | None = None + space_id: str | None = None + if resolved_dataset.is_set(): + if resolved_dataset.is_name() and not resolved_space.is_set(): + raise NotFoundError( + "experiment", + experiment, + hint=( + "Provide 'space' so the dataset name can be resolved, " + "which is needed to resolve the experiment name. Alternatively, " + "you can provide the experiment ID, or the dataset ID instead of the name." + ), + ) + dataset_id = ( + resolved_dataset.id + if resolved_dataset.is_id() + else _find_dataset_id(datasets_api, resolved_dataset.name, space) # type:ignore + ) + else: + # No dataset: resolve a standalone experiment within the space. + space_id = ( + resolved_space.id + if resolved_space.is_id() + else _find_space_id(spaces_api, resolved_space.name) # type:ignore ) - - dataset_id = ( - resolved_dataset.id - if resolved_dataset.is_id() - else _find_dataset_id(datasets_api, resolved_dataset.name, space) # type:ignore - ) available: list[str] = [] + # Only populated when space_id is used (dataset_id is None): unlike a + # dataset-scoped search, a space's experiment list mixes standalone and + # dataset-associated experiments, so a name collision across the two is + # possible. Collect every match before deciding, rather than returning the + # first one, so a collision raises instead of silently returning the + # wrong experiment. + space_scoped_matches: list[str] = [] cursor: str | None = None while True: response = api.list_experiments( dataset_id=dataset_id, + space_id=space_id, limit=_LIST_PAGE_SIZE, cursor=cursor, ) for e in response.experiments: - if e.name == experiment: + if e.name != experiment: + available.append(e.name) + continue + if dataset_id is not None: + # Dataset-scoped: name is unique per dataset, so + # the first match is the only possible match. logger.debug("Resolved experiment '%s' → %s", experiment, e.id) return e.id - available.append(e.name) + space_scoped_matches.append(e.id) cursor = getattr(response.pagination, "next_cursor", None) if not cursor: break + if len(space_scoped_matches) > 1: + raise AmbiguousNameError("experiment", experiment, space_scoped_matches) + if space_scoped_matches: + logger.debug( + "Resolved experiment '%s' → %s", experiment, space_scoped_matches[0] + ) + return space_scoped_matches[0] + raise NotFoundError("experiment", experiment, available) @@ -567,6 +615,78 @@ def _find_ai_integration_id( raise NotFoundError("AI integration", integration, available) +def _find_integration_id( + api: IntegrationsApi, + integration: str, + integration_type: IntegrationType | None, + space: str | None, +) -> str: + """Resolve an integration ID or name to an integration ID. + + Integrations are polymorphic (LLM and agent) and owned at the account level: + a name is unique per ``(account, type)``, so ``integration_type`` alone is + sufficient to resolve a name to an ID — but it is required, since the same + name may exist for each type. IDs resolve without a type. ``space`` is an + optional visibility filter, not required for disambiguation. + + Args: + api: IntegrationsApi instance. + integration: Integration ID or name. + integration_type: The integration type used to scope the name lookup. + Required when *integration* is a name; ignored for IDs. + space: Optional space ID or name used to filter the lookup by visibility. + + Returns: + The resolved integration ID. + + Raises: + NotFoundError: If the integration name cannot be found, or if + *integration* is a name and *integration_type* is not provided. + """ + if is_resource_id(integration): + return integration + + if integration_type is None: + raise NotFoundError( + "integration", + integration, + hint=( + "Provide 'integration_type' so the integration name can be " + "resolved, or provide the integration ID instead of the name." + ), + ) + + resolved_space = _resolve_resource(space) + + available: list[str] = [] + cursor: str | None = None + + while True: + response = api.list_integrations( + type=integration_type, + space_id=resolved_space.id, + space_name=resolved_space.name, + name=integration, + limit=_LIST_PAGE_SIZE, + cursor=cursor, + ) + for item in response.integrations: + inner = item.actual_instance + if inner is None: + continue + if inner.name == integration: + logger.debug( + "Resolved integration '%s' → %s", integration, inner.id + ) + return inner.id + available.append(inner.name) + cursor = getattr(response.pagination, "next_cursor", None) + if not cursor: + break + + raise NotFoundError("integration", integration, available) + + def _find_annotation_queue_id( api: AnnotationQueuesApi, annotation_queue: str, diff --git a/src/arize/utils/unset.py b/src/arize/utils/unset.py new file mode 100644 index 0000000..d0d4063 --- /dev/null +++ b/src/arize/utils/unset.py @@ -0,0 +1,20 @@ +"""Typed sentinel for omitted PATCH arguments.""" + +from typing import Final, TypeGuard, TypeVar, final + +T = TypeVar("T") + + +@final +class UNSET: + """Distinguish an omitted argument from an explicit ``None`` value.""" + + __slots__ = () + + +_UNSET: Final[UNSET] = UNSET() + + +def is_provided(value: T | UNSET) -> TypeGuard[T]: + """Return whether a PATCH argument was explicitly supplied.""" + return not isinstance(value, UNSET) diff --git a/src/arize/version.py b/src/arize/version.py index a2ba85c..5df3181 100644 --- a/src/arize/version.py +++ b/src/arize/version.py @@ -1,3 +1,3 @@ """Version information for the Arize SDK.""" -__version__ = "8.43.1" +__version__ = "8.44.0" diff --git a/tests/integration/test_integrations_flows.py b/tests/integration/test_integrations_flows.py new file mode 100644 index 0000000..0b1513a --- /dev/null +++ b/tests/integration/test_integrations_flows.py @@ -0,0 +1,322 @@ +"""Integration tests for IntegrationsClient end-to-end flows. + +Each test creates real resources, exercises the full lifecycle, and always +cleans up after itself — even on failure. + +Integrations are polymorphic and owned at the account level (a name is unique +per ``(account, type)``), so these flows do not need a space; ``space`` is only +an optional visibility filter on ``get``/``list``/``delete``. + +External-dependency notes: + - Agent creates supply a public HTTPS ``endpoint``; the server validates it + for SSRF (must resolve to a public address) but does not require the + endpoint to be reachable at create time. + - LLM creates supply a provider ``api_key``. The key is stored write-only + and is not validated against the provider until it is actually used, so a + placeholder key is sufficient for CRUD lifecycle testing. + +Run with: + ARIZE_API_KEY= ARIZE_TEST_SPACE_NAME= \ + pytest tests/integration/test_integrations_flows.py -m integration -v +""" + +from __future__ import annotations + +import os +import uuid +from typing import Any + +import pytest + +from arize.integrations.types import ( + CreateAgentRequestPresetInput, + CreateOpenAiConfig, + IntegrationType, +) +from arize.utils.resolve import is_resource_id + +API_KEY = os.environ.get("ARIZE_API_KEY", "") +SPACE_NAME = os.environ.get("ARIZE_TEST_SPACE_NAME", "") + +pytestmark = [ + pytest.mark.integration, + pytest.mark.skipif( + not API_KEY or not SPACE_NAME, + reason="ARIZE_API_KEY and ARIZE_TEST_SPACE_NAME must be set", + ), +] + +_AGENT_INPUT_SCHEMA: dict[str, Any] = { + "type": "object", + "properties": {"input": {"type": "string"}}, + "required": ["input"], +} +_AGENT_ENDPOINT = "https://example.com/agent-replay" + + +def _unique(prefix: str) -> str: + return f"{prefix}-{uuid.uuid4().hex[:8]}" + + +@pytest.fixture(scope="module") +def arize_client() -> Any: + from arize.client import ArizeClient + + return ArizeClient(api_key=API_KEY) + + +@pytest.fixture(scope="module") +def integrations_client(arize_client) -> Any: + return arize_client.integrations + + +class TestAgentIntegrationCRUD: + """End-to-end CRUD flows for agent integrations.""" + + def test_create_get_delete_by_id(self, integrations_client) -> None: + """Create an agent integration, retrieve by ID, delete by ID. + + By-ID operations need no ``integration_type``. + """ + name = _unique("sdk-test-agent") + created = integrations_client.create_agent( + name=name, + endpoint=_AGENT_ENDPOINT, + input_schema=_AGENT_INPUT_SCHEMA, + description="created by SDK integration test", + ) + try: + assert created.name == name + assert created.type == IntegrationType.AGENT.value + assert is_resource_id(created.id) + + fetched = integrations_client.get(integration=created.id) + assert fetched.id == created.id + assert fetched.name == name + finally: + integrations_client.delete(integration=created.id) + + def test_create_get_delete_by_name(self, integrations_client) -> None: + """Resolve an agent integration by name (no space) for get and delete.""" + name = _unique("sdk-test-agent") + created = integrations_client.create_agent( + name=name, + endpoint=_AGENT_ENDPOINT, + input_schema=_AGENT_INPUT_SCHEMA, + ) + try: + fetched = integrations_client.get( + integration=name, + integration_type=IntegrationType.AGENT, + ) + assert fetched.id == created.id + finally: + integrations_client.delete( + integration=name, + integration_type=IntegrationType.AGENT, + ) + + def test_create_with_presets(self, integrations_client) -> None: + """Create an agent integration with an initial request preset.""" + name = _unique("sdk-test-agent") + created = integrations_client.create_agent( + name=name, + endpoint=_AGENT_ENDPOINT, + input_schema=_AGENT_INPUT_SCHEMA, + request_presets=[ + CreateAgentRequestPresetInput( + name="default", + config={"input": "hello"}, + description="the default preset", + ) + ], + ) + try: + assert is_resource_id(created.id) + preset_names = [p.name for p in created.config.request_presets] + assert "default" in preset_names + finally: + integrations_client.delete( + integration=created.id, + integration_type=IntegrationType.AGENT, + ) + + def test_create_appears_in_list(self, integrations_client) -> None: + """A newly created agent integration appears in list() results.""" + name = _unique("sdk-test-agent") + created = integrations_client.create_agent( + name=name, + endpoint=_AGENT_ENDPOINT, + input_schema=_AGENT_INPUT_SCHEMA, + ) + try: + resp = integrations_client.list( + integration_type=IntegrationType.AGENT, limit=100 + ) + ids = [item.id for item in resp.integrations] + assert created.id in ids + finally: + integrations_client.delete( + integration=created.id, + integration_type=IntegrationType.AGENT, + ) + + def test_list_filter_by_name(self, integrations_client) -> None: + """list() name filter returns the matching agent integration.""" + name = _unique("sdk-test-agent") + created = integrations_client.create_agent( + name=name, + endpoint=_AGENT_ENDPOINT, + input_schema=_AGENT_INPUT_SCHEMA, + ) + try: + resp = integrations_client.list( + integration_type=IntegrationType.AGENT, name=name, limit=100 + ) + names = [item.name for item in resp.integrations] + assert name in names + finally: + integrations_client.delete( + integration=created.id, + integration_type=IntegrationType.AGENT, + ) + + def test_update_description_then_clear(self, integrations_client) -> None: + """update_agent sets a description, then clears it with None.""" + name = _unique("sdk-test-agent") + created = integrations_client.create_agent( + name=name, + endpoint=_AGENT_ENDPOINT, + input_schema=_AGENT_INPUT_SCHEMA, + description="initial", + ) + try: + updated = integrations_client.update_agent( + integration=created.id, + description="updated description", + ) + assert updated.description == "updated description" + + cleared = integrations_client.update_agent( + integration=created.id, + description=None, + ) + assert cleared.description is None + finally: + integrations_client.delete( + integration=created.id, + integration_type=IntegrationType.AGENT, + ) + + +class TestLlmIntegrationCRUD: + """End-to-end CRUD flows for LLM integrations.""" + + def test_create_get_delete_by_id(self, integrations_client) -> None: + """Create an OpenAI LLM integration, retrieve by ID, delete by ID. + + By-ID operations need no ``integration_type``. + """ + name = _unique("sdk-test-llm") + created = integrations_client.create_llm( + name=name, + config=CreateOpenAiConfig( + provider="OPEN_AI", api_key="sk-placeholder-not-validated" + ), + ) + try: + assert created.name == name + assert created.type == IntegrationType.LLM.value + assert is_resource_id(created.id) + + fetched = integrations_client.get(integration=created.id) + assert fetched.id == created.id + assert fetched.name == name + finally: + integrations_client.delete(integration=created.id) + + def test_create_get_delete_by_name(self, integrations_client) -> None: + """Resolve an LLM integration by name (no space) for get and delete.""" + name = _unique("sdk-test-llm") + created = integrations_client.create_llm( + name=name, + config=CreateOpenAiConfig( + provider="OPEN_AI", api_key="sk-placeholder-not-validated" + ), + ) + try: + fetched = integrations_client.get( + integration=name, + integration_type=IntegrationType.LLM, + ) + assert fetched.id == created.id + finally: + integrations_client.delete( + integration=name, + integration_type=IntegrationType.LLM, + ) + + def test_update_rename_and_toggle_function_calling( + self, integrations_client + ) -> None: + """update_llm renames the integration and toggles function calling.""" + name = _unique("sdk-test-llm") + new_name = _unique("sdk-test-llm-renamed") + created = integrations_client.create_llm( + name=name, + config=CreateOpenAiConfig( + provider="OPEN_AI", api_key="sk-placeholder-not-validated" + ), + ) + try: + updated = integrations_client.update_llm( + integration=created.id, + name=new_name, + function_calling_enabled=False, + ) + assert updated.name == new_name + + fetched = integrations_client.get( + integration=created.id, + integration_type=IntegrationType.LLM, + ) + assert fetched.name == new_name + finally: + integrations_client.delete( + integration=created.id, + integration_type=IntegrationType.LLM, + ) + + +class TestPolymorphicList: + """The untyped list merges every integration type.""" + + def test_untyped_list_returns_all_types(self, integrations_client) -> None: + """list() without a type returns LLM and agent integrations together.""" + agent_name = _unique("sdk-test-agent") + llm_name = _unique("sdk-test-llm") + created_agent = integrations_client.create_agent( + name=agent_name, + endpoint=_AGENT_ENDPOINT, + input_schema=_AGENT_INPUT_SCHEMA, + ) + try: + created_llm = integrations_client.create_llm( + name=llm_name, + config=CreateOpenAiConfig( + provider="OPEN_AI", api_key="sk-placeholder-not-validated" + ), + ) + try: + resp = integrations_client.list(limit=100) + by_id = {item.id: item for item in resp.integrations} + assert created_agent.id in by_id + assert created_llm.id in by_id + assert ( + by_id[created_agent.id].type == IntegrationType.AGENT.value + ) + assert by_id[created_llm.id].type == IntegrationType.LLM.value + finally: + integrations_client.delete(integration=created_llm.id) + finally: + integrations_client.delete(integration=created_agent.id) diff --git a/tests/integration/test_resolve.py b/tests/integration/test_resolve.py index 5121011..0f02202 100644 --- a/tests/integration/test_resolve.py +++ b/tests/integration/test_resolve.py @@ -381,7 +381,9 @@ def test_id_passthrough( # _find_experiment_id # --------------------------------------------------------------------------- class TestFindExperimentId: - """Tests for _find_experiment_id (requires dataset and datasets_api).""" + """Tests for _find_experiment_id (resolves via dataset, or via space for + a standalone experiment). + """ @pytest.fixture(scope="class") def experiments_api(self, generated_client) -> Any: @@ -425,12 +427,14 @@ def test_resolve_by_name( self, experiments_api, datasets_api, + spaces_api, experiment_info: dict[str, Any], ) -> None: """Resolves experiment name using dataset_id (already an ID, no space needed).""" result = _find_experiment_id( experiments_api, datasets_api, + spaces_api, experiment_info["name"], experiment_info["dataset_id"], None, @@ -441,12 +445,14 @@ def test_id_passthrough( self, experiments_api, datasets_api, + spaces_api, experiment_info: dict[str, Any], ) -> None: """A base64 ID is returned as-is.""" result = _find_experiment_id( experiments_api, datasets_api, + spaces_api, experiment_info["id"], None, None, diff --git a/tests/unit/ai_integrations/test_client.py b/tests/unit/ai_integrations/test_client.py index 3a8a73e..4e4a4cb 100644 --- a/tests/unit/ai_integrations/test_client.py +++ b/tests/unit/ai_integrations/test_client.py @@ -7,7 +7,10 @@ import pytest -from arize._generated.api_client import AIIntegrationsApi +from arize._generated.api_client import ( + AIIntegrationsApi, + UpdateAiIntegrationRequest, +) from arize.ai_integrations.client import AiIntegrationsClient # Base64 ID that decodes to "Integration:123" — passes _is_resource_id() @@ -338,6 +341,23 @@ def test_update_explicit_none_is_forwarded( api_key=None, ) + def test_update_omits_none_name( + self, ai_integrations_client: AiIntegrationsClient, mock_api: Mock + ) -> None: + """update() should omit None rather than send a null integration name.""" + ai_integrations_client.update( + integration=_INTEGRATION_ID, + name=None, + api_key="new-api-key", + ) + + body = mock_api.update_ai_integration.call_args.kwargs[ + "update_ai_integration_request" + ] + assert isinstance(body, UpdateAiIntegrationRequest) + assert body.model_fields_set == {"api_key"} + assert body.to_dict() == {"api_key": "new-api-key"} + def test_update_no_fields_sends_empty_request( self, ai_integrations_client: AiIntegrationsClient, mock_api: Mock ) -> None: diff --git a/tests/unit/annotation_queues/test_client.py b/tests/unit/annotation_queues/test_client.py index 2196d7e..5ce493a 100644 --- a/tests/unit/annotation_queues/test_client.py +++ b/tests/unit/annotation_queues/test_client.py @@ -341,21 +341,60 @@ def test_raises_when_no_fields_provided( with pytest.raises(ValueError, match="At least one of"): annotation_queues_client.update(annotation_queue=_QUEUE_ID) - def test_empty_string_instructions_sends_through( + def test_none_instructions_clears_existing_instructions( self, annotation_queues_client: AnnotationQueuesClient, mock_api: Mock ) -> None: - """update() should send instructions='' as-is to clear it on the server.""" - with patch( - "arize._generated.api_client.UpdateAnnotationQueueRequest" - ) as mock_body_cls: - mock_body_cls.return_value = Mock() + """update() should serialize an explicit null to clear instructions.""" + annotation_queues_client.update( + annotation_queue=_QUEUE_ID, + instructions=None, + ) - annotation_queues_client.update( - annotation_queue=_QUEUE_ID, - instructions="", - ) + body = mock_api.update_annotation_queue.call_args.kwargs[ + "update_annotation_queue_request" + ] + assert body.model_fields_set == {"instructions"} + assert body.to_dict() == {"instructions": None} + + def test_empty_lists_clear_existing_values( + self, annotation_queues_client: AnnotationQueuesClient, mock_api: Mock + ) -> None: + """update() should serialize empty lists to clear existing values.""" + annotation_queues_client.update( + annotation_queue=_QUEUE_ID, + annotation_config_ids=[], + annotator_emails=[], + ) - mock_body_cls.assert_called_once_with(instructions="") + body = mock_api.update_annotation_queue.call_args.kwargs[ + "update_annotation_queue_request" + ] + assert body.model_fields_set == { + "annotation_config_ids", + "annotator_emails", + } + assert body.to_dict() == { + "annotation_config_ids": [], + "annotator_emails": [], + } + + def test_none_non_nullable_fields_are_omitted( + self, annotation_queues_client: AnnotationQueuesClient, mock_api: Mock + ) -> None: + """update() should only serialize None for nullable instructions.""" + annotation_queues_client.update( + annotation_queue=_QUEUE_ID, + name=None, + annotation_config_ids=None, + annotator_emails=None, + instructions="Review carefully.", + ) + + body = mock_api.update_annotation_queue.call_args.kwargs[ + "update_annotation_queue_request" + ] + assert body.model_fields_set == {"instructions"} + assert body.to_dict() == {"instructions": "Review carefully."} def test_returns_api_response( self, annotation_queues_client: AnnotationQueuesClient, mock_api: Mock diff --git a/tests/unit/evaluators/test_client.py b/tests/unit/evaluators/test_client.py index 3a32005..d748a7f 100644 --- a/tests/unit/evaluators/test_client.py +++ b/tests/unit/evaluators/test_client.py @@ -7,7 +7,7 @@ import pytest -from arize._generated.api_client import EvaluatorsApi +from arize._generated.api_client import EvaluatorsApi, UpdateEvaluatorRequest from arize.evaluators.client import EvaluatorsClient from arize.evaluators.types import ( CodeConfig, @@ -483,58 +483,90 @@ class TestEvaluatorsClientUpdate: def test_update_with_name( self, evaluators_client: EvaluatorsClient, mock_api: Mock ) -> None: - """update() should build UpdateEvaluatorRequest with only name when only name is given.""" - with patch( - "arize._generated.api_client.UpdateEvaluatorRequest" - ) as mock_request_cls: - mock_body = Mock() - mock_request_cls.return_value = mock_body - - evaluators_client.update(evaluator=_EVALUATOR_ID, name="new-name") - - mock_request_cls.assert_called_once_with( - name="new-name", description=None - ) + """update() should set only a provided name in its request body.""" + evaluators_client.update(evaluator=_EVALUATOR_ID, name="new-name") + + body = mock_api.update_evaluator.call_args.kwargs[ + "update_evaluator_request" + ] + assert isinstance(body, UpdateEvaluatorRequest) + assert body.model_fields_set == {"name"} + assert body.to_dict() == {"name": "new-name"} mock_api.update_evaluator.assert_called_once_with( evaluator_id=_EVALUATOR_ID, - update_evaluator_request=mock_body, + update_evaluator_request=body, ) def test_update_with_description( self, evaluators_client: EvaluatorsClient, mock_api: Mock ) -> None: - """update() should forward description to UpdateEvaluatorRequest.""" - with patch( - "arize._generated.api_client.UpdateEvaluatorRequest" - ) as mock_request_cls: - mock_request_cls.return_value = Mock() + """update() should set only a provided description in its request body.""" + evaluators_client.update( + evaluator=_EVALUATOR_ID, description="Updated description" + ) - evaluators_client.update( - evaluator=_EVALUATOR_ID, description="Updated description" - ) + body = mock_api.update_evaluator.call_args.kwargs[ + "update_evaluator_request" + ] + assert body.model_fields_set == {"description"} + assert body.to_dict() == {"description": "Updated description"} - mock_request_cls.assert_called_once_with( - name=None, description="Updated description" + def test_update_with_both_fields( + self, evaluators_client: EvaluatorsClient, mock_api: Mock + ) -> None: + """update() should set both concrete metadata values in its request body.""" + evaluators_client.update( + evaluator=_EVALUATOR_ID, + name="new-name", + description="new description", ) - def test_update_with_both_fields( + body = mock_api.update_evaluator.call_args.kwargs[ + "update_evaluator_request" + ] + assert body.model_fields_set == {"name", "description"} + assert body.to_dict() == { + "name": "new-name", + "description": "new description", + } + + def test_update_omits_unprovided_fields( self, evaluators_client: EvaluatorsClient, mock_api: Mock ) -> None: - """update() should forward both name and description.""" - with patch( - "arize._generated.api_client.UpdateEvaluatorRequest" - ) as mock_request_cls: - mock_request_cls.return_value = Mock() + """update() should leave unprovided fields absent from the request.""" + evaluators_client.update(evaluator=_EVALUATOR_ID) - evaluators_client.update( - evaluator=_EVALUATOR_ID, - name="new-name", - description="new description", - ) + body = mock_api.update_evaluator.call_args.kwargs[ + "update_evaluator_request" + ] + assert body.model_fields_set == set() + assert body.to_dict() == {} - mock_request_cls.assert_called_once_with( - name="new-name", description="new description" - ) + def test_update_omits_none_name( + self, + evaluators_client: EvaluatorsClient, + mock_api: Mock, + ) -> None: + """update() should leave a ``None`` name absent from its request body.""" + evaluators_client.update(evaluator=_EVALUATOR_ID, name=None) + + body = mock_api.update_evaluator.call_args.kwargs[ + "update_evaluator_request" + ] + assert body.model_fields_set == set() + assert body.to_dict() == {} + + def test_update_includes_explicit_none_to_clear_description( + self, evaluators_client: EvaluatorsClient, mock_api: Mock + ) -> None: + """update() should send an explicit ``None`` to clear the description.""" + evaluators_client.update(evaluator=_EVALUATOR_ID, description=None) + + body = mock_api.update_evaluator.call_args.kwargs[ + "update_evaluator_request" + ] + assert body.model_fields_set == {"description"} + assert body.to_dict() == {"description": None} def test_update_returns_api_response( self, evaluators_client: EvaluatorsClient, mock_api: Mock @@ -543,8 +575,7 @@ def test_update_returns_api_response( expected = Mock() mock_api.update_evaluator.return_value = expected - with patch("arize._generated.api_client.UpdateEvaluatorRequest"): - result = evaluators_client.update(evaluator=_EVALUATOR_ID, name="x") + result = evaluators_client.update(evaluator=_EVALUATOR_ID, name="x") assert result is expected @@ -559,8 +590,7 @@ def test_update_emits_beta_prerelease_warning( pre_releases._WARNED.clear() caplog.set_level(logging.WARNING) - with patch("arize._generated.api_client.UpdateEvaluatorRequest"): - evaluators_client.update(evaluator=_EVALUATOR_ID, name="x") + evaluators_client.update(evaluator=_EVALUATOR_ID, name="x") assert any( "BETA" in record.message and "evaluators.update" in record.message diff --git a/tests/unit/experiments/test_client.py b/tests/unit/experiments/test_client.py index 611033b..d1084fd 100644 --- a/tests/unit/experiments/test_client.py +++ b/tests/unit/experiments/test_client.py @@ -9,6 +9,7 @@ from arize._generated.api_client import ExperimentsApi from arize.experiments.client import ExperimentsClient +from arize.experiments.types import ExperimentTaskFieldNames @pytest.fixture @@ -52,6 +53,161 @@ def run_experiment_df() -> pd.DataFrame: return df +@pytest.mark.unit +class TestList: + """Tests for ExperimentsClient.list scoping.""" + + def test_space_only_sends_space_id( + self, + experiments_client: ExperimentsClient, + mock_api: Mock, + ) -> None: + """A space without a dataset must filter by space_id — the only way to + list experiments that aren't associated with a dataset. + """ + with patch( + "arize.experiments.client._find_space_id", + return_value="space-id-123", + ) as mock_find_space: + experiments_client.list(space="my-space") + + mock_find_space.assert_called_once() + kwargs = mock_api.list_experiments.call_args.kwargs + assert kwargs["space_id"] == "space-id-123" + assert kwargs["dataset_id"] is None + + def test_dataset_wins_when_both_given( + self, + experiments_client: ExperimentsClient, + mock_api: Mock, + ) -> None: + """The endpoint rejects both scopes, so dataset — the narrower one — + must win and space must only resolve the dataset name. + """ + with ( + patch( + "arize.experiments.client._find_dataset_id", + return_value="dataset-id-456", + ), + patch("arize.experiments.client._find_space_id") as mock_find_space, + ): + experiments_client.list(dataset="my-dataset", space="my-space") + + mock_find_space.assert_not_called() + kwargs = mock_api.list_experiments.call_args.kwargs + assert kwargs["dataset_id"] == "dataset-id-456" + assert kwargs["space_id"] is None + + def test_neither_scope_sends_no_filter( + self, + experiments_client: ExperimentsClient, + mock_api: Mock, + ) -> None: + """No scope still means every experiment the caller can read.""" + experiments_client.list() + + kwargs = mock_api.list_experiments.call_args.kwargs + assert kwargs["dataset_id"] is None + assert kwargs["space_id"] is None + + def test_forwards_pagination_arguments( + self, + experiments_client: ExperimentsClient, + mock_api: Mock, + ) -> None: + """Limit and cursor must reach the generated client unchanged.""" + experiments_client.list(cursor="opaque-cursor", limit=25) + + kwargs = mock_api.list_experiments.call_args.kwargs + assert kwargs["limit"] == 25 + assert kwargs["cursor"] == "opaque-cursor" + + +@pytest.mark.unit +class TestCreate: + """Tests for ExperimentsClient.create.""" + + def test_standalone_uses_space_id_and_skips_example_id( + self, + experiments_client: ExperimentsClient, + mock_api: Mock, + mock_sdk_config: Mock, + ) -> None: + """A standalone (space-only) create must send space_id, not dataset_id, + and must not require example_id on runs. + """ + mock_sdk_config.max_http_payload_size_mb = 100 + mock_api.create_experiment.return_value = Mock() + + with patch( + "arize.experiments.client._find_space_id", + return_value="space-id-123", + ) as mock_find_space: + experiments_client.create( + name="standalone-exp", + space="my-space", + experiment_runs=[{"output": "4"}], + task_fields=ExperimentTaskFieldNames(output="output"), + ) + + mock_find_space.assert_called_once() + mock_api.create_experiment.assert_called_once() + body = mock_api.create_experiment.call_args.kwargs[ + "create_experiment_request" + ] + assert body.dataset_id is None + assert body.space_id == "space-id-123" + assert len(body.experiment_runs) == 1 + assert body.experiment_runs[0].example_id is None + assert body.experiment_runs[0].output == "4" + + def test_dataset_backed_sets_dataset_id_not_space_id( + self, + experiments_client: ExperimentsClient, + mock_api: Mock, + mock_sdk_config: Mock, + ) -> None: + """A dataset-backed create must be unaffected: dataset_id set, space_id + absent, example_id still required and forwarded. + """ + mock_sdk_config.max_http_payload_size_mb = 100 + mock_api.create_experiment.return_value = Mock() + + with patch( + "arize.experiments.client._find_dataset_id", + return_value="dataset-id-456", + ): + experiments_client.create( + name="dataset-exp", + dataset="my-dataset", + experiment_runs=[{"example_id": "ex-1", "output": "out"}], + task_fields=ExperimentTaskFieldNames( + example_id="example_id", output="output" + ), + ) + + body = mock_api.create_experiment.call_args.kwargs[ + "create_experiment_request" + ] + assert body.dataset_id == "dataset-id-456" + assert body.space_id is None + assert body.experiment_runs[0].example_id == "ex-1" + + def test_raises_value_error_without_dataset_or_space( + self, + experiments_client: ExperimentsClient, + ) -> None: + """Neither dataset nor space is a validation error, raised before any + API call. + """ + with pytest.raises(ValueError, match="Either 'dataset' or 'space'"): + experiments_client.create( + name="no-target", + experiment_runs=[{"output": "x"}], + task_fields=ExperimentTaskFieldNames(output="output"), + ) + + @pytest.mark.unit class TestAppendRuns: """Tests for ExperimentsClient.append_runs.""" @@ -175,10 +331,7 @@ def test_cache_write_skipped_when_caching_disabled( experiment_obj = Mock() experiment_obj.updated_at = "2024-01-01T00:00:00Z" - experiment_obj.dataset_id = "RGF0YXNldDoxMjM6YWJj" - - dataset_obj = Mock() - dataset_obj.space_id = "space-123" + experiment_obj.space_id = "space-123" experiment_df = pd.DataFrame( { @@ -190,9 +343,6 @@ def test_cache_write_skipped_when_caching_disabled( with ( patch.object(client, "get", return_value=experiment_obj), - patch.object( - client._datasets_api, "datasets_get", return_value=dataset_obj - ), patch( "arize.experiments.client.load_cached_resource", return_value=None, @@ -222,6 +372,10 @@ def test_cache_write_skipped_when_caching_disabled( assert response.experiment_runs[0].output == '{"ok": true}' mock_cache_write.assert_not_called() + mock_flight_instance.get_experiment_runs.assert_called_once_with( + space_id="space-123", + experiment_id="RXhwZXJpbWVudDoxMjM6YWJj", + ) def test_cache_write_called_when_caching_enabled( self, mock_sdk_config: Mock @@ -231,18 +385,12 @@ def test_cache_write_called_when_caching_enabled( experiment_obj = Mock() experiment_obj.updated_at = "2024-01-01T00:00:00Z" - experiment_obj.dataset_id = "RGF0YXNldDoxMjM6YWJj" - - dataset_obj = Mock() - dataset_obj.space_id = "space-123" + experiment_obj.space_id = "space-123" empty_df = pd.DataFrame(columns=["id", "example_id", "output"]) with ( patch.object(client, "get", return_value=experiment_obj), - patch.object( - client._datasets_api, "datasets_get", return_value=dataset_obj - ), patch( "arize.experiments.client.load_cached_resource", return_value=None, @@ -274,13 +422,18 @@ def test_cache_write_called_when_caching_enabled( ) -class TestListRunsNoDataset: - """Tests for ExperimentsClient.list_runs(all=True) on dataset-less experiments.""" +@pytest.mark.unit +class TestListRunsStandalone: + """Tests for ExperimentsClient.list_runs(all=True) on standalone + (dataset-less) experiments. + """ - def test_raises_value_error_when_experiment_has_no_dataset( + def test_uses_experiment_space_id_without_calling_get_dataset( self, mock_sdk_config: Mock ) -> None: - """list_runs(all=True) must raise rather than call get_dataset(dataset_id=None).""" + """list_runs(all=True) must succeed for a standalone experiment, + using experiment.space_id directly rather than resolving a dataset. + """ with ( patch( "arize._generated.api_client.ExperimentsApi", @@ -294,19 +447,42 @@ def test_raises_value_error_when_experiment_has_no_dataset( sdk_config=mock_sdk_config, generated_client=Mock(), ) + mock_sdk_config.enable_caching = False experiment_obj = Mock() experiment_obj.dataset_id = None + experiment_obj.space_id = "space-456" + experiment_obj.updated_at = "2024-01-01T00:00:00Z" + + empty_df = pd.DataFrame(columns=["id", "example_id", "output"]) with ( patch.object(client, "get", return_value=experiment_obj), patch.object( client._datasets_api, "get_dataset" ) as mock_get_dataset, - pytest.raises(ValueError, match="no associated dataset"), + patch( + "arize.experiments.client.load_cached_resource", + return_value=None, + ), + patch( + "arize.experiments.client.ArizeFlightClient" + ) as mock_flight_cls, ): + mock_flight_instance = MagicMock() + mock_flight_instance.__enter__ = Mock( + return_value=mock_flight_instance + ) + mock_flight_instance.__exit__ = Mock(return_value=False) + mock_flight_instance.get_experiment_runs.return_value = empty_df + mock_flight_cls.return_value = mock_flight_instance + # Use a base64-encoded ID so _find_experiment_id treats it as a # direct resource ID and skips the name-lookup API call. client.list_runs(experiment="RXhwZXJpbWVudDoxMjM6YWJj", all=True) mock_get_dataset.assert_not_called() + mock_flight_instance.get_experiment_runs.assert_called_once_with( + space_id="space-456", + experiment_id="RXhwZXJpbWVudDoxMjM6YWJj", + ) diff --git a/tests/unit/integrations/__init__.py b/tests/unit/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/integrations/test_client.py b/tests/unit/integrations/test_client.py new file mode 100644 index 0000000..a015bd4 --- /dev/null +++ b/tests/unit/integrations/test_client.py @@ -0,0 +1,868 @@ +"""Unit tests for src/arize/integrations/client.py.""" + +from __future__ import annotations + +import logging +from unittest.mock import Mock, create_autospec, patch + +import pytest + +from arize._generated import api_client as gen +from arize._generated.api_client import IntegrationsApi +from arize.integrations.client import IntegrationsClient +from arize.integrations.types import ( + IntegrationType, + ListIntegrationsResponse, +) +from arize.utils.resolve import NotFoundError + +# Base64 ID that decodes to "Integration:123" — passes is_resource_id() +_INTEGRATION_ID = "SW50ZWdyYXRpb246MTIz" + + +@pytest.fixture +def mock_api() -> Mock: + """Provide a mock IntegrationsApi instance.""" + return create_autospec(IntegrationsApi, instance=True) + + +@pytest.fixture +def integrations_client( + mock_sdk_config: Mock, mock_api: Mock +) -> IntegrationsClient: + """Provide an IntegrationsClient with mocked internals.""" + with patch( + "arize._generated.api_client.IntegrationsApi", return_value=mock_api + ): + return IntegrationsClient( + sdk_config=mock_sdk_config, + generated_client=Mock(), + ) + + +@pytest.mark.unit +class TestIntegrationsClientInit: + """Tests for IntegrationsClient.__init__().""" + + def test_stores_sdk_config( + self, mock_sdk_config: Mock, mock_api: Mock + ) -> None: + """Constructor should store sdk_config on the instance.""" + with patch( + "arize._generated.api_client.IntegrationsApi", + return_value=mock_api, + ): + client = IntegrationsClient( + sdk_config=mock_sdk_config, + generated_client=Mock(), + ) + assert client._sdk_config is mock_sdk_config + + def test_creates_api_with_generated_client( + self, mock_sdk_config: Mock + ) -> None: + """Constructor should pass generated_client to IntegrationsApi.""" + mock_generated_client = Mock() + with patch( + "arize._generated.api_client.IntegrationsApi" + ) as mock_api_cls: + IntegrationsClient( + sdk_config=mock_sdk_config, + generated_client=mock_generated_client, + ) + mock_api_cls.assert_called_once_with(mock_generated_client) + + +@pytest.mark.unit +class TestIntegrationsClientList: + """Tests for IntegrationsClient.list().""" + + @pytest.fixture(autouse=True) + def _bypass_model_validate(self) -> None: + with patch.object( + ListIntegrationsResponse, + "model_validate", + side_effect=lambda v, **kw: v, + ): + yield + + def test_list_with_space_id( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """list() should resolve a base64 resource ID space value to space_id.""" + integrations_client.list( + integration_type=IntegrationType.LLM, + name="my-integration", + space="U3BhY2U6OTA1MDoxSmtS", + limit=25, + cursor="cursor-xyz", + ) + + mock_api.list_integrations.assert_called_once_with( + type=IntegrationType.LLM, + space_id="U3BhY2U6OTA1MDoxSmtS", + space_name=None, + name="my-integration", + limit=25, + cursor="cursor-xyz", + ) + + def test_list_with_space_name( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """list() should resolve a non-prefixed space value to space_name.""" + integrations_client.list( + integration_type=IntegrationType.AGENT, + space="my-space", + ) + + mock_api.list_integrations.assert_called_once_with( + type=IntegrationType.AGENT, + space_id=None, + space_name="my-space", + name=None, + limit=50, + cursor=None, + ) + + def test_list_defaults( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """list() with no arguments should request all types (type=None).""" + integrations_client.list() + + mock_api.list_integrations.assert_called_once_with( + type=None, + space_id=None, + space_name=None, + name=None, + limit=50, + cursor=None, + ) + + def test_list_returns_wrapped_response( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """list() should propagate the (bypassed) validated response.""" + expected = Mock() + mock_api.list_integrations.return_value = expected + + result = integrations_client.list(integration_type=IntegrationType.LLM) + + assert result is expected + + def test_list_emits_alpha_prerelease_warning( + self, + integrations_client: IntegrationsClient, + caplog: pytest.LogCaptureFixture, + ) -> None: + """First call should emit the ALPHA prerelease warning.""" + from arize import pre_releases + + pre_releases._WARNED.clear() + caplog.set_level(logging.WARNING) + + integrations_client.list(integration_type=IntegrationType.LLM) + + assert any( + "ALPHA" in record.message and "integrations.list" in record.message + for record in caplog.records + ) + + +@pytest.mark.unit +class TestIntegrationsClientGet: + """Tests for IntegrationsClient.get().""" + + def test_get_calls_api_with_integration_id( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """get() by ID should not require a type and skip resolution.""" + integrations_client.get(integration=_INTEGRATION_ID) + + mock_api.get_integration.assert_called_once_with( + integration_id=_INTEGRATION_ID + ) + mock_api.list_integrations.assert_not_called() + + def test_get_by_name_without_type_raises( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """get() by name should require integration_type to resolve.""" + with pytest.raises(NotFoundError, match="integration_type"): + integrations_client.get(integration="my-integration") + + mock_api.list_integrations.assert_not_called() + mock_api.get_integration.assert_not_called() + + def test_get_unwraps_response( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """get() should unwrap the oneOf response to its actual_instance.""" + expected = Mock() + mock_api.get_integration.return_value.actual_instance = expected + + result = integrations_client.get( + integration=_INTEGRATION_ID, integration_type=IntegrationType.AGENT + ) + + assert result is expected + + def test_get_resolves_name_without_space( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """get() should resolve a name using type alone (space not required). + + Integration names are unique per ``(account, type)``, so ``type`` is + sufficient to resolve a name to an ID; ``space`` is only a visibility + filter. + """ + match = Mock() + match.actual_instance.name = "my-integration" + match.actual_instance.id = _INTEGRATION_ID + mock_api.list_integrations.return_value.integrations = [match] + mock_api.list_integrations.return_value.pagination.next_cursor = None + + integrations_client.get( + integration="my-integration", + integration_type=IntegrationType.LLM, + ) + + # Resolution lists by type with no space filter, then fetches by ID. + mock_api.list_integrations.assert_called_once_with( + type=IntegrationType.LLM, + space_id=None, + space_name=None, + name="my-integration", + limit=100, + cursor=None, + ) + mock_api.get_integration.assert_called_once_with( + integration_id=_INTEGRATION_ID + ) + + +def _created_llm_config(mock_api: Mock) -> object: + """Extract the provider config passed through create_integration(). + + Returns the ``actual_instance`` of the wrapped ``CreateLlmConfig`` — i.e. + the concrete per-provider ``Create*Config`` object the client forwarded. + """ + body = mock_api.create_integration.call_args.kwargs[ + "create_integration_request" + ] + inner = body.actual_instance + assert isinstance(inner, gen.CreateLlmIntegrationRequest) + assert inner.type == "LLM" + assert isinstance(inner.config, gen.CreateLlmConfig) + return inner.config.actual_instance + + +@pytest.mark.unit +class TestIntegrationsClientCreateLlm: + """Tests for IntegrationsClient.create_llm() across all 7 providers.""" + + def test_create_openai_builds_request( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm(OPEN_AI) should wrap and forward the OpenAI config.""" + config = gen.CreateOpenAiConfig( + provider="OPEN_AI", + api_key="sk-abc", + is_function_calling_enabled=True, + ) + integrations_client.create_llm(name="Prod OpenAI", config=config) + + mock_api.create_integration.assert_called_once() + assert _created_llm_config(mock_api) is config + body = mock_api.create_integration.call_args.kwargs[ + "create_integration_request" + ] + assert body.actual_instance.name == "Prod OpenAI" + assert body.actual_instance.scopings is None + + def test_create_anthropic_builds_request( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm(ANTHROPIC) should forward the Anthropic config.""" + config = gen.CreateAnthropicConfig( + provider="ANTHROPIC", api_key="sk-ant" + ) + integrations_client.create_llm(name="Prod Anthropic", config=config) + + forwarded = _created_llm_config(mock_api) + assert isinstance(forwarded, gen.CreateAnthropicConfig) + assert forwarded.api_key == "sk-ant" + + def test_create_gemini_builds_request( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm(GEMINI) should forward the Gemini config.""" + config = gen.CreateGeminiConfig(provider="GEMINI", api_key="sk-gem") + integrations_client.create_llm(name="Gemini", config=config) + + assert _created_llm_config(mock_api) is config + + def test_create_vertex_ai_builds_request( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm(VERTEX_AI) should forward the Vertex AI config.""" + config = gen.CreateVertexAiConfig( + provider="VERTEX_AI", + project_id="proj-1", + location="us-central1", + project_access_label="label", + ) + integrations_client.create_llm(name="Vertex", config=config) + + forwarded = _created_llm_config(mock_api) + assert isinstance(forwarded, gen.CreateVertexAiConfig) + assert forwarded.location == "us-central1" + + def test_create_custom_builds_request( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm(CUSTOM) should forward the Custom config.""" + config = gen.CreateCustomConfig( + provider="CUSTOM", + base_url="https://custom.example.com", + api_key="sk-custom", + headers={"x-team": "ml"}, + model_names=["my-model"], + ) + integrations_client.create_llm(name="Custom", config=config) + + forwarded = _created_llm_config(mock_api) + assert isinstance(forwarded, gen.CreateCustomConfig) + assert forwarded.base_url == "https://custom.example.com" + + def test_create_nvidia_nim_builds_request( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm(NVIDIA_NIM) should forward the NVIDIA NIM config.""" + config = gen.CreateNvidiaNimConfig( + provider="NVIDIA_NIM", + base_url="https://nim.example.com", + is_default_models_enabled=True, + ) + integrations_client.create_llm(name="NIM", config=config) + + forwarded = _created_llm_config(mock_api) + assert isinstance(forwarded, gen.CreateNvidiaNimConfig) + assert forwarded.is_default_models_enabled is True + + def test_create_bedrock_default_auth( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm(AWS_BEDROCK) with DEFAULT auth should forward correctly.""" + config = gen.CreateAwsBedrockConfig( + provider="AWS_BEDROCK", + auth=gen.CreateAwsBedrockAuth( + actual_instance=gen.CreateAwsBedrockDefaultAuth( + auth_type="DEFAULT", + role_arn="arn:aws:iam::123:role/arize", + external_id="ext-1", + ) + ), + is_default_models_enabled=True, + ) + integrations_client.create_llm(name="Bedrock Default", config=config) + + forwarded = _created_llm_config(mock_api) + assert isinstance(forwarded, gen.CreateAwsBedrockConfig) + auth = forwarded.auth.actual_instance + assert isinstance(auth, gen.CreateAwsBedrockDefaultAuth) + assert auth.role_arn == "arn:aws:iam::123:role/arize" + + def test_create_bedrock_bearer_token_auth( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm(AWS_BEDROCK) with BEARER_TOKEN auth should forward it.""" + config = gen.CreateAwsBedrockConfig( + provider="AWS_BEDROCK", + auth=gen.CreateAwsBedrockAuth( + actual_instance=gen.CreateAwsBedrockBearerTokenAuth( + auth_type="BEARER_TOKEN", + api_key="bearer-xyz", + ) + ), + model_names=["anthropic.claude"], + ) + integrations_client.create_llm(name="Bedrock Bearer", config=config) + + forwarded = _created_llm_config(mock_api) + auth = forwarded.auth.actual_instance + assert isinstance(auth, gen.CreateAwsBedrockBearerTokenAuth) + assert auth.api_key == "bearer-xyz" + + def test_create_bedrock_proxy_with_headers_auth( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm(AWS_BEDROCK) with PROXY_WITH_HEADERS auth forwards it.""" + config = gen.CreateAwsBedrockConfig( + provider="AWS_BEDROCK", + auth=gen.CreateAwsBedrockAuth( + actual_instance=gen.CreateAwsBedrockProxyWithHeadersAuth( + auth_type="PROXY_WITH_HEADERS", + base_url="https://proxy.example.com", + headers={"x-api": "v"}, + ) + ), + model_names=["anthropic.claude"], + ) + integrations_client.create_llm(name="Bedrock Proxy", config=config) + + forwarded = _created_llm_config(mock_api) + auth = forwarded.auth.actual_instance + assert isinstance(auth, gen.CreateAwsBedrockProxyWithHeadersAuth) + assert auth.base_url == "https://proxy.example.com" + + def test_create_llm_accepts_prewrapped_config( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm() should accept a pre-wrapped CreateLlmConfig union.""" + inner = gen.CreateOpenAiConfig(provider="OPEN_AI", api_key="sk") + wrapped = gen.CreateLlmConfig(actual_instance=inner) + integrations_client.create_llm(name="n", config=wrapped) + + body = mock_api.create_integration.call_args.kwargs[ + "create_integration_request" + ] + assert body.actual_instance.config is wrapped + + def test_create_llm_forwards_scopings( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm() should forward provided scopings to the request.""" + scopings = [gen.IntegrationScoping(space_id="sp-1")] + config = gen.CreateOpenAiConfig(provider="OPEN_AI", api_key="sk") + integrations_client.create_llm( + name="n", config=config, scopings=scopings + ) + + body = mock_api.create_integration.call_args.kwargs[ + "create_integration_request" + ] + assert body.actual_instance.scopings == scopings + + def test_create_llm_unwraps_response( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_llm() should unwrap the oneOf response.""" + expected = Mock() + mock_api.create_integration.return_value.actual_instance = expected + + config = gen.CreateOpenAiConfig(provider="OPEN_AI", api_key="sk") + result = integrations_client.create_llm(name="n", config=config) + + assert result is expected + + +@pytest.mark.unit +class TestIntegrationsClientCreateAgent: + """Tests for IntegrationsClient.create_agent().""" + + def test_create_agent_builds_request( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_agent() should build the agent config and request.""" + schema = {"type": "object"} + with ( + patch( + "arize._generated.api_client.CreateAgentConfig" + ) as mock_cfg_cls, + patch( + "arize._generated.api_client.CreateAgentIntegrationRequest" + ) as mock_req_cls, + patch( + "arize._generated.api_client.CreateIntegrationRequest" + ) as mock_env_cls, + ): + integrations_client.create_agent( + name="My Agent", + endpoint="https://agent.example.com/run", + input_schema=schema, + description="desc", + headers={"x-key": "v"}, + ) + + mock_cfg_cls.assert_called_once_with( + endpoint="https://agent.example.com/run", + input_schema=schema, + headers={"x-key": "v"}, + request_presets=None, + ) + mock_req_cls.assert_called_once_with( + type="AGENT", + name="My Agent", + description="desc", + scopings=None, + config=mock_cfg_cls.return_value, + ) + mock_env_cls.assert_called_once_with( + actual_instance=mock_req_cls.return_value + ) + mock_api.create_integration.assert_called_once_with( + create_integration_request=mock_env_cls.return_value + ) + + def test_create_agent_unwraps_response( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """create_agent() should unwrap the oneOf response.""" + expected = Mock() + mock_api.create_integration.return_value.actual_instance = expected + + with ( + patch("arize._generated.api_client.CreateAgentConfig"), + patch("arize._generated.api_client.CreateAgentIntegrationRequest"), + patch("arize._generated.api_client.CreateIntegrationRequest"), + ): + result = integrations_client.create_agent( + name="n", + endpoint="https://x", + input_schema={"type": "object"}, + ) + + assert result is expected + + +@pytest.mark.unit +class TestIntegrationsClientUpdateLlm: + """Tests for IntegrationsClient.update_llm().""" + + def test_update_only_sends_provided_fields( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() should only send caller-provided envelope fields.""" + with ( + patch( + "arize._generated.api_client.UpdateLlmIntegrationRequest" + ) as mock_req_cls, + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_llm( + integration=_INTEGRATION_ID, + name="Updated", + ) + + mock_req_cls.assert_called_once_with(type="LLM", name="Updated") + mock_api.update_integration.assert_called_once() + + def test_update_builds_config_for_api_key( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() should build a config when api_key is provided.""" + with ( + patch( + "arize._generated.api_client.UpdateLlmConfig" + ) as mock_cfg_cls, + patch( + "arize._generated.api_client.UpdateLlmIntegrationRequest" + ) as mock_req_cls, + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_llm( + integration=_INTEGRATION_ID, + api_key="sk-new", + function_calling_enabled=False, + ) + + mock_cfg_cls.assert_called_once_with( + api_key="sk-new", + is_function_calling_enabled=False, + ) + mock_req_cls.assert_called_once_with( + type="LLM", config=mock_cfg_cls.return_value + ) + + def test_update_explicit_none_api_key_clears( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() should forward explicit None api_key to clear it.""" + with ( + patch( + "arize._generated.api_client.UpdateLlmConfig" + ) as mock_cfg_cls, + patch("arize._generated.api_client.UpdateLlmIntegrationRequest"), + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_llm( + integration=_INTEGRATION_ID, + api_key=None, + ) + + mock_cfg_cls.assert_called_once_with(api_key=None) + + def test_update_bedrock_auth( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() should build a config from a replacement Bedrock auth.""" + auth = gen.CreateAwsBedrockAuth( + actual_instance=gen.CreateAwsBedrockDefaultAuth( + auth_type="DEFAULT", role_arn="arn:aws:iam::123:role/r" + ) + ) + with ( + patch( + "arize._generated.api_client.UpdateLlmConfig" + ) as mock_cfg_cls, + patch("arize._generated.api_client.UpdateLlmIntegrationRequest"), + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_llm( + integration=_INTEGRATION_ID, + auth=auth, + model_names=["anthropic.claude"], + is_default_models_enabled=True, + ) + + mock_cfg_cls.assert_called_once_with( + auth=auth, + is_default_models_enabled=True, + model_names=["anthropic.claude"], + ) + + def test_update_custom_base_url_and_headers( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() should forward base_url and headers for CUSTOM.""" + with ( + patch( + "arize._generated.api_client.UpdateLlmConfig" + ) as mock_cfg_cls, + patch("arize._generated.api_client.UpdateLlmIntegrationRequest"), + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_llm( + integration=_INTEGRATION_ID, + base_url="https://new.example.com", + headers={"x-team": "ml"}, + ) + + mock_cfg_cls.assert_called_once_with( + base_url="https://new.example.com", + headers={"x-team": "ml"}, + ) + + def test_update_nim_base_url_and_headers_clear( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() should forward explicit None to clear base_url/headers.""" + with ( + patch( + "arize._generated.api_client.UpdateLlmConfig" + ) as mock_cfg_cls, + patch("arize._generated.api_client.UpdateLlmIntegrationRequest"), + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_llm( + integration=_INTEGRATION_ID, + base_url=None, + headers=None, + ) + + mock_cfg_cls.assert_called_once_with(base_url=None, headers=None) + + def test_update_vertex_ai_fields( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() should forward Vertex AI project fields.""" + with ( + patch( + "arize._generated.api_client.UpdateLlmConfig" + ) as mock_cfg_cls, + patch("arize._generated.api_client.UpdateLlmIntegrationRequest"), + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_llm( + integration=_INTEGRATION_ID, + project_id="proj-2", + location="us-east1", + project_access_label="label-2", + ) + + mock_cfg_cls.assert_called_once_with( + project_id="proj-2", + location="us-east1", + project_access_label="label-2", + ) + + def test_update_no_fields_raises( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() with no updatable fields should raise ValueError.""" + with pytest.raises(ValueError, match="At least one field"): + integrations_client.update_llm(integration=_INTEGRATION_ID) + + mock_api.update_integration.assert_not_called() + + def test_update_replaces_scopings( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() should include scopings when provided.""" + scopings = [Mock()] + with ( + patch( + "arize._generated.api_client.UpdateLlmIntegrationRequest" + ) as mock_req_cls, + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_llm( + integration=_INTEGRATION_ID, + scopings=scopings, + ) + + mock_req_cls.assert_called_once_with(type="LLM", scopings=scopings) + + def test_update_llm_unwraps_response( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_llm() should unwrap the oneOf response.""" + expected = Mock() + mock_api.update_integration.return_value.actual_instance = expected + + with ( + patch("arize._generated.api_client.UpdateLlmIntegrationRequest"), + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + result = integrations_client.update_llm( + integration=_INTEGRATION_ID, name="x" + ) + + assert result is expected + + +@pytest.mark.unit +class TestIntegrationsClientUpdateAgent: + """Tests for IntegrationsClient.update_agent().""" + + def test_update_builds_config_and_envelope( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_agent() should build config from provided config fields.""" + schema = {"type": "object"} + with ( + patch( + "arize._generated.api_client.UpdateAgentConfig" + ) as mock_cfg_cls, + patch( + "arize._generated.api_client.UpdateAgentIntegrationRequest" + ) as mock_req_cls, + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_agent( + integration=_INTEGRATION_ID, + name="Updated Agent", + endpoint="https://new.example.com", + input_schema=schema, + ) + + mock_cfg_cls.assert_called_once_with( + endpoint="https://new.example.com", + input_schema=schema, + ) + mock_req_cls.assert_called_once_with( + type="AGENT", + name="Updated Agent", + config=mock_cfg_cls.return_value, + ) + + def test_update_explicit_none_clears_nullable_fields( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_agent() should forward explicit None for description/headers.""" + with ( + patch( + "arize._generated.api_client.UpdateAgentConfig" + ) as mock_cfg_cls, + patch( + "arize._generated.api_client.UpdateAgentIntegrationRequest" + ) as mock_req_cls, + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_agent( + integration=_INTEGRATION_ID, + description=None, + headers=None, + ) + + mock_cfg_cls.assert_called_once_with(headers=None) + mock_req_cls.assert_called_once_with( + type="AGENT", + description=None, + config=mock_cfg_cls.return_value, + ) + + def test_update_no_fields_raises( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_agent() with no updatable fields should raise ValueError.""" + with pytest.raises(ValueError, match="At least one field"): + integrations_client.update_agent(integration=_INTEGRATION_ID) + + mock_api.update_integration.assert_not_called() + + def test_update_agent_replaces_scopings_and_presets( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """update_agent() should include scopings and request_presets when given.""" + presets = [Mock()] + scopings = [Mock()] + with ( + patch( + "arize._generated.api_client.UpdateAgentConfig" + ) as mock_cfg_cls, + patch( + "arize._generated.api_client.UpdateAgentIntegrationRequest" + ) as mock_req_cls, + patch("arize._generated.api_client.UpdateIntegrationRequest"), + ): + integrations_client.update_agent( + integration=_INTEGRATION_ID, + request_presets=presets, + scopings=scopings, + ) + + mock_cfg_cls.assert_called_once_with(request_presets=presets) + mock_req_cls.assert_called_once_with( + type="AGENT", + scopings=scopings, + config=mock_cfg_cls.return_value, + ) + + +@pytest.mark.unit +class TestIntegrationsClientDelete: + """Tests for IntegrationsClient.delete().""" + + def test_delete_calls_api_with_integration_id( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """delete() by ID should not require a type and skip resolution.""" + integrations_client.delete(integration=_INTEGRATION_ID) + + mock_api.delete_integration.assert_called_once_with( + integration_id=_INTEGRATION_ID + ) + mock_api.list_integrations.assert_not_called() + + def test_delete_by_name_without_type_raises( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """delete() by name should require integration_type to resolve.""" + with pytest.raises(NotFoundError, match="integration_type"): + integrations_client.delete(integration="my-integration") + + mock_api.list_integrations.assert_not_called() + mock_api.delete_integration.assert_not_called() + + def test_delete_returns_none( + self, integrations_client: IntegrationsClient, mock_api: Mock + ) -> None: + """delete() should return None on success.""" + result = integrations_client.delete( + integration=_INTEGRATION_ID, integration_type=IntegrationType.LLM + ) + + assert result is None diff --git a/tests/unit/integrations/test_types.py b/tests/unit/integrations/test_types.py new file mode 100644 index 0000000..e2bafaf --- /dev/null +++ b/tests/unit/integrations/test_types.py @@ -0,0 +1,178 @@ +"""Tests for arize.integrations.types public re-exports and unwrapping.""" + +from __future__ import annotations + +from enum import Enum +from unittest.mock import Mock + +import pytest + +import arize.integrations.types as types_module +from arize._generated.api_client.models.agent_integration import ( + AgentIntegration, +) +from arize._generated.api_client.models.integration import Integration +from arize._generated.api_client.models.llm_integration import LlmIntegration +from arize._generated.api_client.models.pagination_metadata import ( + PaginationMetadata, +) +from arize.integrations.types import ( + IntegrationType, + ListIntegrationsResponse, + LlmIntegrationProvider, +) + + +@pytest.mark.unit +class TestIntegrationsTypes: + """Tests for the integrations types module re-exports.""" + + def test_all_exports_are_accessible(self) -> None: + """Every name in __all__ should be accessible as a module attribute.""" + for name in types_module.__all__: + assert hasattr(types_module, name), f"{name} missing from module" + assert getattr(types_module, name) is not None, f"{name} is None" + + def test_expected_names_in_all(self) -> None: + """__all__ should contain the expected public type names.""" + assert "IntegrationType" in types_module.__all__ + assert "AgentIntegration" in types_module.__all__ + assert "LlmIntegration" in types_module.__all__ + assert "ListIntegrationsResponse" in types_module.__all__ + assert "AgentRequestPreset" in types_module.__all__ + assert "CreateAgentRequestPresetInput" in types_module.__all__ + assert "UpdateAgentRequestPresetInput" in types_module.__all__ + + def test_all_seven_read_configs_exported(self) -> None: + """All 7 provider read config types should be re-exported.""" + for name in ( + "OpenAiConfig", + "AnthropicConfig", + "GeminiConfig", + "AwsBedrockConfig", + "CustomConfig", + "VertexAiConfig", + "NvidiaNimConfig", + ): + assert name in types_module.__all__ + + def test_all_seven_create_configs_and_auth_exported(self) -> None: + """All 7 create config types plus Bedrock auth variants are exported.""" + for name in ( + "CreateOpenAiConfig", + "CreateAnthropicConfig", + "CreateGeminiConfig", + "CreateAwsBedrockConfig", + "CreateCustomConfig", + "CreateVertexAiConfig", + "CreateNvidiaNimConfig", + "CreateLlmConfig", + "UpdateLlmConfig", + "AwsBedrockAuth", + "AwsBedrockDefaultAuth", + "AwsBedrockBearerTokenAuth", + "AwsBedrockProxyWithHeadersAuth", + "CreateAwsBedrockAuth", + "CreateAwsBedrockDefaultAuth", + "CreateAwsBedrockBearerTokenAuth", + "CreateAwsBedrockProxyWithHeadersAuth", + ): + assert name in types_module.__all__ + + def test_integration_type_is_enum(self) -> None: + assert issubclass(IntegrationType, Enum) + + def test_llm_integration_provider_is_enum(self) -> None: + assert issubclass(LlmIntegrationProvider, Enum) + + def test_list_integrations_response_is_class(self) -> None: + assert isinstance(ListIntegrationsResponse, type) + + +def _agent_integration(name: str = "agent-1") -> AgentIntegration: + return AgentIntegration.from_dict( + { + "id": "id-agent", + "type": "AGENT", + "name": name, + "description": None, + "scopings": [], + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "created_by_user_id": "u1", + "config": { + "endpoint": "https://a.example.com", + "has_headers": False, + "input_schema": {"type": "object"}, + "request_presets": [], + }, + } + ) + + +def _llm_integration(name: str = "llm-1") -> LlmIntegration: + return LlmIntegration.from_dict( + { + "id": "id-llm", + "type": "LLM", + "name": name, + "scopings": [], + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "created_by_user_id": "u1", + "config": { + "provider": "OPEN_AI", + "has_api_key": True, + "is_function_calling_enabled": True, + }, + } + ) + + +@pytest.mark.unit +class TestListIntegrationsResponseUnwrap: + """Tests for ListIntegrationsResponse oneOf unwrapping.""" + + def test_unwraps_wrapped_integrations(self) -> None: + """Each wrapped Integration should be replaced by its actual_instance.""" + agent = _agent_integration() + llm = _llm_integration() + source = Mock() + source.integrations = [ + Integration(actual_instance=agent), + Integration(actual_instance=llm), + ] + source.pagination = PaginationMetadata(has_more=False, next_cursor=None) + + result = ListIntegrationsResponse.model_validate( + source, from_attributes=True + ) + + assert result.integrations == [agent, llm] + assert isinstance(result.integrations[0], AgentIntegration) + assert isinstance(result.integrations[1], LlmIntegration) + + def test_accepts_already_unwrapped_items(self) -> None: + """Concrete (non-wrapped) items should pass through unchanged.""" + llm = _llm_integration() + source = Mock() + source.integrations = [llm] + source.pagination = PaginationMetadata(has_more=True, next_cursor="abc") + + result = ListIntegrationsResponse.model_validate( + source, from_attributes=True + ) + + assert result.integrations == [llm] + + def test_raises_when_actual_instance_is_none(self) -> None: + """A wrapper with actual_instance=None should raise a ValueError.""" + empty = Integration.model_construct(actual_instance=None) + source = Mock() + source.integrations = [empty] + source.pagination = PaginationMetadata(has_more=False, next_cursor=None) + + with pytest.raises(ValueError, match="actual_instance=None"): + ListIntegrationsResponse.model_validate( + source, from_attributes=True + ) diff --git a/tests/unit/organizations/test_client.py b/tests/unit/organizations/test_client.py index a564858..1a0efae 100644 --- a/tests/unit/organizations/test_client.py +++ b/tests/unit/organizations/test_client.py @@ -253,6 +253,65 @@ def test_update_builds_request_and_calls_api( update_organization_request=mock_body, ) + def test_update_clears_description_when_explicitly_none( + self, organizations_client: OrganizationsClient + ) -> None: + """update() should send null when clearing a description.""" + with patch( + "arize._generated.api_client.UpdateOrganizationRequest" + ) as mock_request_cls: + organizations_client.update( + organization="T3JnYW5pemF0aW9uOjEyMzQ1", + description=None, + ) + + mock_request_cls.assert_called_once_with(description=None) + + def test_update_omits_none_name_and_raises_when_no_fields_remain( + self, organizations_client: OrganizationsClient + ) -> None: + """update() should treat name=None as an omitted field.""" + with pytest.raises( + ValueError, + match="At least one of 'name' or 'description' must be provided", + ): + organizations_client.update( + organization="T3JnYW5pemF0aW9uOjEyMzQ1", + name=None, + ) + + def test_update_request_omits_unset_description( + self, organizations_client: OrganizationsClient, mock_api: Mock + ) -> None: + """A name-only update should not serialize the default description.""" + organizations_client.update( + organization="T3JnYW5pemF0aW9uOjEyMzQ1", + name="updated-org", + ) + + body = mock_api.update_organization.call_args.kwargs[ + "update_organization_request" + ] + + assert body.model_fields_set == {"name"} + assert body.to_dict() == {"name": "updated-org"} + + def test_update_request_serializes_none_description_as_clear( + self, organizations_client: OrganizationsClient, mock_api: Mock + ) -> None: + """An explicit null description should remain in the wire payload.""" + organizations_client.update( + organization="T3JnYW5pemF0aW9uOjEyMzQ1", + description=None, + ) + + body = mock_api.update_organization.call_args.kwargs[ + "update_organization_request" + ] + + assert body.model_fields_set == {"description"} + assert body.to_dict() == {"description": None} + def test_update_returns_api_response( self, organizations_client: OrganizationsClient, mock_api: Mock ) -> None: diff --git a/tests/unit/prompts/test_client.py b/tests/unit/prompts/test_client.py index 59a8164..b0e4d24 100644 --- a/tests/unit/prompts/test_client.py +++ b/tests/unit/prompts/test_client.py @@ -7,7 +7,7 @@ import pytest -from arize._generated.api_client import PromptsApi +from arize._generated.api_client import PromptsApi, UpdatePromptRequest from arize.prompts.client import PromptsClient # Base64 ID that decodes to "Prompt:123" — passes _is_resource_id() @@ -328,26 +328,41 @@ class TestPromptsClientUpdate: def test_update_builds_request_and_calls_api( self, prompts_client: PromptsClient, mock_api: Mock ) -> None: - """update() should build UpdatePromptRequest and pass it to prompts_update.""" - with patch( - "arize._generated.api_client.UpdatePromptRequest" - ) as mock_request_cls: - mock_body = Mock() - mock_request_cls.return_value = mock_body - - prompts_client.update( - prompt=_PROMPT_ID, - description="updated description", - ) - - mock_request_cls.assert_called_once_with( - description="updated description" + """update() should serialize a concrete description in its request body.""" + prompts_client.update( + prompt=_PROMPT_ID, + description="updated description", ) + body = mock_api.update_prompt.call_args.kwargs["update_prompt_request"] + assert isinstance(body, UpdatePromptRequest) + assert body.model_fields_set == {"description"} + assert body.to_dict() == {"description": "updated description"} mock_api.update_prompt.assert_called_once_with( prompt_id=_PROMPT_ID, - update_prompt_request=mock_body, + update_prompt_request=body, ) + def test_update_requires_description( + self, prompts_client: PromptsClient, mock_api: Mock + ) -> None: + """update() should reject requests without a mutable field.""" + with pytest.raises( + TypeError, match="missing 1 required keyword-only argument" + ): + prompts_client.update(prompt=_PROMPT_ID) + + mock_api.update_prompt.assert_not_called() + + def test_update_includes_explicit_none_to_clear_description( + self, prompts_client: PromptsClient, mock_api: Mock + ) -> None: + """update() should send an explicit ``None`` to clear the description.""" + prompts_client.update(prompt=_PROMPT_ID, description=None) + + body = mock_api.update_prompt.call_args.kwargs["update_prompt_request"] + assert body.model_fields_set == {"description"} + assert body.to_dict() == {"description": None} + def test_update_returns_api_response( self, prompts_client: PromptsClient, mock_api: Mock ) -> None: @@ -355,11 +370,10 @@ def test_update_returns_api_response( expected = Mock() mock_api.update_prompt.return_value = expected - with patch("arize._generated.api_client.UpdatePromptRequest"): - result = prompts_client.update( - prompt=_PROMPT_ID, - description="updated", - ) + result = prompts_client.update( + prompt=_PROMPT_ID, + description="updated", + ) assert result is expected diff --git a/tests/unit/roles/test_client.py b/tests/unit/roles/test_client.py index cc1bc73..4f4ce83 100644 --- a/tests/unit/roles/test_client.py +++ b/tests/unit/roles/test_client.py @@ -307,19 +307,42 @@ def test_update_builds_request_and_calls_api( def test_update_with_only_name( self, roles_client: RolesClient, mock_api: Mock ) -> None: - """update() with only name should pass None for other fields.""" - with patch( - "arize._generated.api_client.UpdateRoleRequest" - ) as mock_request_cls: - mock_request_cls.return_value = Mock() + """update() should omit fields which were not supplied.""" + roles_client.update(role=_ROLE_ID, name="New Name") - roles_client.update(role=_ROLE_ID, name="New Name") + body = mock_api.update_role.call_args.kwargs["update_role_request"] + assert body.model_fields_set == {"name"} + assert body.to_dict() == {"name": "New Name"} - mock_request_cls.assert_called_once_with( - name="New Name", - description=None, - permissions=None, - ) + def test_update_with_none_description_clears_description( + self, roles_client: RolesClient, mock_api: Mock + ) -> None: + """update() should serialize an explicit null description.""" + roles_client.update(role=_ROLE_ID, description=None) + + body = mock_api.update_role.call_args.kwargs["update_role_request"] + assert body.model_fields_set == {"description"} + assert body.to_dict() == {"description": None} + + def test_update_with_none_permissions_omits_permissions( + self, roles_client: RolesClient, mock_api: Mock + ) -> None: + """update() should preserve permissions when passed None.""" + roles_client.update(role=_ROLE_ID, permissions=None, name="New Name") + + body = mock_api.update_role.call_args.kwargs["update_role_request"] + assert body.model_fields_set == {"name"} + assert body.to_dict() == {"name": "New Name"} + + def test_update_with_permissions_replaces_permissions( + self, roles_client: RolesClient, mock_api: Mock + ) -> None: + """update() should serialize a supplied replacement permission list.""" + roles_client.update(role=_ROLE_ID, permissions=["PROJECT_READ"]) + + body = mock_api.update_role.call_args.kwargs["update_role_request"] + assert body.model_fields_set == {"permissions"} + assert body.to_dict() == {"permissions": ["PROJECT_READ"]} def test_update_returns_api_response( self, roles_client: RolesClient, mock_api: Mock diff --git a/tests/unit/spaces/test_client.py b/tests/unit/spaces/test_client.py index d8396d1..c4cecb2 100644 --- a/tests/unit/spaces/test_client.py +++ b/tests/unit/spaces/test_client.py @@ -338,6 +338,19 @@ def test_update_raises_when_no_fields_provided( ): spaces_client.update(space="U3BhY2U6OTA1MDoxSmtS") + def test_update_raises_when_only_none_is_private_is_provided( + self, spaces_client: SpacesClient + ) -> None: + """update() should treat is_private=None as an omitted field.""" + with pytest.raises( + ValueError, + match="At least one of 'name', 'description', or 'is_private' must be provided", + ): + spaces_client.update( + space="U3BhY2U6OTA1MDoxSmtS", + is_private=None, + ) + def test_update_builds_request_and_calls_api( self, spaces_client: SpacesClient, mock_api: Mock ) -> None: @@ -357,7 +370,6 @@ def test_update_builds_request_and_calls_api( mock_request_cls.assert_called_once_with( name="updated-space", description="updated description", - is_private=None, ) mock_api.update_space.assert_called_once_with( space_id="U3BhY2U6OTA1MDoxSmtS", @@ -406,11 +418,94 @@ def test_update_with_is_private_true_passes_flag_to_api( ) mock_request_cls.assert_called_once_with( - name=None, - description=None, is_private=True, ) + def test_update_clears_description_when_explicitly_none( + self, spaces_client: SpacesClient + ) -> None: + """update() should send null when clearing a description.""" + with patch( + "arize._generated.api_client.UpdateSpaceRequest" + ) as mock_request_cls: + spaces_client.update( + space="U3BhY2U6OTA1MDoxSmtS", + description=None, + ) + + mock_request_cls.assert_called_once_with(description=None) + + def test_update_omits_none_is_private_to_preserve_visibility( + self, spaces_client: SpacesClient + ) -> None: + """update() should omit is_private=None rather than clear visibility.""" + with patch( + "arize._generated.api_client.UpdateSpaceRequest" + ) as mock_request_cls: + spaces_client.update( + space="U3BhY2U6OTA1MDoxSmtS", + name="updated-space", + is_private=None, + ) + + mock_request_cls.assert_called_once_with(name="updated-space") + + def test_update_retains_false_is_private( + self, spaces_client: SpacesClient + ) -> None: + """update() should send False when making a space public.""" + with patch( + "arize._generated.api_client.UpdateSpaceRequest" + ) as mock_request_cls: + spaces_client.update( + space="U3BhY2U6OTA1MDoxSmtS", + is_private=False, + ) + + mock_request_cls.assert_called_once_with(is_private=False) + + def test_update_request_omits_unset_description( + self, spaces_client: SpacesClient, mock_api: Mock + ) -> None: + """A name-only update should not serialize the default description.""" + spaces_client.update( + space="U3BhY2U6OTA1MDoxSmtS", + name="updated-space", + ) + + body = mock_api.update_space.call_args.kwargs["update_space_request"] + + assert body.model_fields_set == {"name"} + assert body.to_dict() == {"name": "updated-space"} + + def test_update_request_serializes_none_description_as_clear( + self, spaces_client: SpacesClient, mock_api: Mock + ) -> None: + """An explicit null description should remain in the wire payload.""" + spaces_client.update( + space="U3BhY2U6OTA1MDoxSmtS", + description=None, + ) + + body = mock_api.update_space.call_args.kwargs["update_space_request"] + + assert body.model_fields_set == {"description"} + assert body.to_dict() == {"description": None} + + def test_update_request_serializes_concrete_visibility_value( + self, spaces_client: SpacesClient, mock_api: Mock + ) -> None: + """A concrete visibility value should remain in the wire payload.""" + spaces_client.update( + space="U3BhY2U6OTA1MDoxSmtS", + is_private=False, + ) + + body = mock_api.update_space.call_args.kwargs["update_space_request"] + + assert body.model_fields_set == {"is_private"} + assert body.to_dict() == {"is_private": False} + def test_update_emits_private_space_warning( self, spaces_client: SpacesClient, diff --git a/tests/unit/spans/test_client.py b/tests/unit/spans/test_client.py index 78ec5e0..8ebfc0f 100644 --- a/tests/unit/spans/test_client.py +++ b/tests/unit/spans/test_client.py @@ -121,6 +121,8 @@ def test_delete_builds_request_and_calls_api( mock_request_cls.assert_called_once_with( project_id=_PROJECT_ID, span_ids=["span-1", "span-2"], + start_time=None, + end_time=None, ) mock_api.delete_spans.assert_called_once_with( delete_spans_request=mock_body, @@ -191,6 +193,33 @@ def test_delete_with_project_name_resolves_id( mock_request_cls.assert_called_once_with( project_id=_PROJECT_ID, span_ids=["span-1"], + start_time=None, + end_time=None, + ) + + def test_delete_passes_time_bounds( + self, spans_client: SpansClient, mock_api: Mock + ) -> None: + """delete() should forward start_time/end_time to DeleteSpansRequest.""" + start = datetime(2024, 6, 1, tzinfo=timezone.utc) + end = datetime(2024, 6, 2, tzinfo=timezone.utc) + + with patch( + "arize._generated.api_client.DeleteSpansRequest" + ) as mock_request_cls: + mock_request_cls.return_value = Mock() + spans_client.delete( + project=_PROJECT_ID, + span_ids=["span-1"], + start_time=start, + end_time=end, + ) + + mock_request_cls.assert_called_once_with( + project_id=_PROJECT_ID, + span_ids=["span-1"], + start_time=start, + end_time=end, ) def test_delete_emits_beta_prerelease_warning( diff --git a/tests/unit/tasks/test_client.py b/tests/unit/tasks/test_client.py index acac728..2f8251d 100644 --- a/tests/unit/tasks/test_client.py +++ b/tests/unit/tasks/test_client.py @@ -8,6 +8,9 @@ import pytest from arize._generated.api_client import TasksApi +from arize._generated.api_client.models.agent_call_run_config import ( + AgentCallRunConfig, +) from arize._generated.api_client.models.run_configuration import ( RunConfiguration, ) @@ -22,6 +25,7 @@ _TASK_ID = "VGFzazoxMjM=" # Task:123 _PROJECT_ID = "UHJvamVjdDoxMjM=" # Project:123 _DATASET_ID = "RGF0YXNldDoxMjM=" # Dataset:123 +_INTEGRATION_ID = "SW50ZWdyYXRpb246MTIz" # Integration:123 @pytest.fixture @@ -396,6 +400,36 @@ def test_create_run_experiment_builds_correct_request( create_task_request=mock_wrapper ) + def test_create_run_experiment_accepts_agent_call_run_config( + self, tasks_client: TasksClient, mock_api: Mock + ) -> None: + """create(RUN_EXPERIMENT) should wrap an unwrapped AgentCallRunConfig.""" + agent_config = AgentCallRunConfig( + experiment_type="AGENT_CALL", + integration_id=_INTEGRATION_ID, + input_template={"prompt": "{{input}}"}, + ) + + with ( + patch( + "arize._generated.api_client.CreateRunExperimentTaskRequest" + ) as mock_inner_cls, + patch("arize._generated.api_client.CreateTaskRequest"), + ): + mock_inner_cls.return_value = Mock() + + tasks_client._create( + name="agent-task", + task_type="RUN_EXPERIMENT", + run_configuration=agent_config, + dataset=_DATASET_ID, + ) + + _, kwargs = mock_inner_cls.call_args + wrapped = kwargs["run_configuration"] + assert isinstance(wrapped, RunConfiguration) + assert wrapped.actual_instance is agent_config + def test_create_run_experiment_rejects_eval_only_fields( self, tasks_client: TasksClient, mock_api: Mock ) -> None: @@ -720,6 +754,96 @@ def test_update_with_query_filter_none( mock_inner_cls.assert_called_once_with(query_filter=None) + def test_update_run_experiment_accepts_agent_call_run_config( + self, tasks_client: TasksClient, mock_api: Mock + ) -> None: + """update() on a run_experiment task should wrap an AgentCallRunConfig.""" + mock_api.get_task.return_value.type = "RUN_EXPERIMENT" + agent_config = AgentCallRunConfig( + experiment_type="AGENT_CALL", + integration_id=_INTEGRATION_ID, + input_template={"prompt": "{{input}}"}, + ) + + with ( + patch( + "arize._generated.api_client.UpdateRunExperimentTaskRequest" + ) as mock_inner_cls, + patch("arize._generated.api_client.UpdateTaskRequest"), + ): + mock_inner_cls.return_value = Mock() + + tasks_client.update( + task=_TASK_ID, + run_configuration=agent_config, + ) + + _, kwargs = mock_inner_cls.call_args + wrapped = kwargs["run_configuration"] + assert isinstance(wrapped, RunConfiguration) + assert wrapped.actual_instance is agent_config + + def test_update_run_experiment_builds_request( + self, tasks_client: TasksClient, mock_api: Mock + ) -> None: + """update() should use the run-experiment request schema.""" + mock_api.get_task.return_value.type = "RUN_EXPERIMENT" + run_configuration = Mock(spec=RunConfiguration) + + with ( + patch( + "arize._generated.api_client.UpdateRunExperimentTaskRequest" + ) as mock_inner_cls, + patch( + "arize._generated.api_client.UpdateTaskRequest" + ) as mock_wrapper_cls, + ): + mock_inner = Mock() + mock_inner_cls.return_value = mock_inner + mock_wrapper = Mock() + mock_wrapper_cls.return_value = mock_wrapper + + tasks_client.update( + task=_TASK_ID, + name="new-name", + run_configuration=run_configuration, + ) + + mock_inner_cls.assert_called_once_with( + name="new-name", + run_configuration=run_configuration, + ) + mock_wrapper_cls.assert_called_once_with(actual_instance=mock_inner) + mock_api.update_task.assert_called_once_with( + task_id=_TASK_ID, + update_task_request=mock_wrapper, + ) + + def test_update_run_experiment_rejects_evaluation_fields( + self, tasks_client: TasksClient, mock_api: Mock + ) -> None: + """update() should reject evaluation fields for run-experiment tasks.""" + mock_api.get_task.return_value.type = "RUN_EXPERIMENT" + + with pytest.raises(ValueError, match="sampling_rate"): + tasks_client.update(task=_TASK_ID, sampling_rate=0.25) + + mock_api.update_task.assert_not_called() + + def test_update_evaluation_rejects_run_configuration( + self, tasks_client: TasksClient, mock_api: Mock + ) -> None: + """update() should reject run configuration for evaluation tasks.""" + mock_api.get_task.return_value.type = "TEMPLATE_EVALUATION" + + with pytest.raises(ValueError, match="run_configuration"): + tasks_client.update( + task=_TASK_ID, + run_configuration=Mock(spec=RunConfiguration), + ) + + mock_api.update_task.assert_not_called() + def test_update_raises_when_no_fields( self, tasks_client: TasksClient, mock_api: Mock ) -> None: diff --git a/tests/unit/tasks/test_types.py b/tests/unit/tasks/test_types.py index b4036d7..59335b6 100644 --- a/tests/unit/tasks/test_types.py +++ b/tests/unit/tasks/test_types.py @@ -7,6 +7,9 @@ import pytest import arize.tasks.types as types_module +from arize._generated.api_client.models.agent_call_run_config import ( + AgentCallRunConfig, +) from arize._generated.api_client.models.llm_generation_run_config import ( LlmGenerationRunConfig, ) @@ -30,6 +33,14 @@ # --------------------------------------------------------------------------- +def _make_agent_call_run_config() -> AgentCallRunConfig: + return AgentCallRunConfig.model_construct( + experiment_type="AGENT_CALL", + integration_id="integration_1", + input_template={"prompt": "{{input}}"}, + ) + + def _make_llm_run_config() -> LlmGenerationRunConfig: return LlmGenerationRunConfig.model_construct( experiment_type="LLM_GENERATION", @@ -82,6 +93,7 @@ def test_expected_names_in_all(self) -> None: """__all__ should contain the expected public type names.""" expected = { "TaskEvaluatorInput", + "AgentCallRunConfig", "LlmGenerationRunConfig", "Task", "TaskRun", @@ -95,6 +107,7 @@ def test_expected_names_in_all(self) -> None: "cls", [ TaskEvaluatorInput, + AgentCallRunConfig, LlmGenerationRunConfig, Task, TaskRun, @@ -122,6 +135,17 @@ def test_unwraps_llm_generation_run_config_from_wrapper(self) -> None: assert task.run_configuration is llm_config + def test_unwraps_agent_call_run_config_from_wrapper(self) -> None: + """RunConfiguration wrapping AgentCallRunConfig should be unwrapped.""" + agent_config = _make_agent_call_run_config() + wrapper = _GenRunConfiguration.model_construct( + actual_instance=agent_config + ) + + task = _make_task(run_configuration=wrapper) + + assert task.run_configuration is agent_config + def test_unwraps_template_evaluation_run_config_from_wrapper(self) -> None: """RunConfiguration wrapping TemplateEvaluationRunConfig should be unwrapped.""" tmpl_config = _make_template_run_config() @@ -150,6 +174,14 @@ def test_passes_through_llm_run_config_directly(self) -> None: assert task.run_configuration is llm_config + def test_passes_through_agent_call_run_config_directly(self) -> None: + """AgentCallRunConfig passed directly should not be transformed.""" + agent_config = _make_agent_call_run_config() + + task = _make_task(run_configuration=agent_config) + + assert task.run_configuration is agent_config + def test_passes_through_template_run_config_directly(self) -> None: """TemplateEvaluationRunConfig passed directly should not be transformed.""" tmpl_config = _make_template_run_config() diff --git a/tests/unit/test_generated_models_forward_compat.py b/tests/unit/test_generated_models_forward_compat.py new file mode 100644 index 0000000..23ded95 --- /dev/null +++ b/tests/unit/test_generated_models_forward_compat.py @@ -0,0 +1,82 @@ +"""Regression tests for forward-compatible generated model deserialization.""" + +from __future__ import annotations + +import pytest + +from arize._generated.api_client.models.create_project_request import ( + CreateProjectRequest, +) +from arize._generated.api_client.models.project import Project +from arize._generated.api_client.models.task_run import TaskRun + +_TASK_RUN = { + "id": "run_1", + "task_id": "task_1", + "experiment_id": None, + "status": "COMPLETED", + "run_started_at": None, + "run_finished_at": None, + "data_start_time": None, + "data_end_time": None, + "num_successes": 5, + "num_errors": 0, + "num_skipped": 0, + "created_at": "2026-07-23T00:00:00Z", + "created_by_user_id": None, + "failure_reason": None, +} + + +@pytest.mark.unit +class TestTaskRunForwardCompatibility: + """Guard the response deserialization failure reported in #80452.""" + + def test_tolerates_unknown_fields(self) -> None: + run = TaskRun.from_dict({**_TASK_RUN, "future_field_xyz": "new_value"}) + + assert run is not None + assert run.additional_properties["future_field_xyz"] == "new_value" + + def test_failure_reason_deserializes(self) -> None: + run = TaskRun.from_dict( + { + **_TASK_RUN, + "status": "CANCELLED", + "failure_reason": "all data already has evaluation labels", + } + ) + + assert run is not None + assert run.status == "CANCELLED" + assert run.failure_reason == "all data already has evaluation labels" + + +@pytest.mark.unit +class TestResponseGenerationPolicy: + """Distinguish response tolerance from strict request validation.""" + + def test_response_model_ignores_unknown_fields(self) -> None: + project = Project.from_dict( + { + "id": "project_1", + "name": "example", + "space_id": "space_1", + "created_at": "2026-07-23T00:00:00Z", + "updated_at": "2026-07-23T00:00:00Z", + "future_field_xyz": "new_value", + } + ) + + assert project is not None + assert not hasattr(project, "additional_properties") + + def test_request_model_rejects_unknown_fields(self) -> None: + with pytest.raises(ValueError, match="future_field_xyz"): + CreateProjectRequest.from_dict( + { + "name": "example", + "space_id": "space_1", + "future_field_xyz": "new_value", + } + ) diff --git a/tests/unit/utils/test_space.py b/tests/unit/utils/test_space.py index 43a7c32..b08cc3d 100644 --- a/tests/unit/utils/test_space.py +++ b/tests/unit/utils/test_space.py @@ -14,6 +14,7 @@ _find_dataset_id, _find_evaluator_id, _find_experiment_id, + _find_integration_id, _find_project_id, _find_prompt_id, _find_space_id, @@ -38,10 +39,13 @@ def _make_paginated(items: list, next_cursor: str | None = None) -> MagicMock: return resp -def _item(name: str, id: str = "some-id") -> MagicMock: +def _item( + name: str, id: str = "some-id", dataset_id: str | None = None +) -> MagicMock: item = MagicMock() item.name = name item.id = id + item.dataset_id = dataset_id return item @@ -310,14 +314,21 @@ def test_pagination(self) -> None: class TestFindExperimentId: def test_base64_passthrough(self) -> None: assert ( - _find_experiment_id(MagicMock(), MagicMock(), B64_ID, None, None) + _find_experiment_id( + MagicMock(), MagicMock(), MagicMock(), B64_ID, None, None + ) == B64_ID ) - def test_no_dataset_id_raises(self) -> None: + def test_no_dataset_or_space_raises(self) -> None: with pytest.raises(NotFoundError, match="experiment"): _find_experiment_id( - MagicMock(), MagicMock(), "my-experiment", None, None + MagicMock(), + MagicMock(), + MagicMock(), + "my-experiment", + None, + None, ) def test_name_resolved(self) -> None: @@ -327,17 +338,73 @@ def test_name_resolved(self) -> None: mock_api.list_experiments.return_value = resp # Use B64_ID as dataset so _find_dataset_id is skipped (direct ID passthrough) result = _find_experiment_id( - mock_api, MagicMock(), "my-experiment", B64_ID, None + mock_api, MagicMock(), MagicMock(), "my-experiment", B64_ID, None ) assert result == "exp-id" + def test_name_resolved_standalone_via_space(self) -> None: + """No dataset provided: resolves a standalone experiment by name + within the space instead. + """ + resp = _make_paginated([]) + resp.experiments = [_item("my-experiment", "exp-id")] + mock_api = MagicMock() + mock_api.list_experiments.return_value = resp + # Use B64_ID as space so _find_space_id is skipped (direct ID passthrough) + result = _find_experiment_id( + mock_api, MagicMock(), MagicMock(), "my-experiment", None, B64_ID + ) + assert result == "exp-id" + mock_api.list_experiments.assert_called_once() + assert mock_api.list_experiments.call_args.kwargs["space_id"] == B64_ID + + def test_name_resolved_via_space_when_match_is_dataset_backed(self) -> None: + """No dataset provided, and the only name match in the space is a + dataset-associated experiment (not standalone): still resolves it, + since there's no collision to disambiguate. + """ + resp = _make_paginated([]) + resp.experiments = [_item("my-experiment", "exp-id", dataset_id="ds-1")] + mock_api = MagicMock() + mock_api.list_experiments.return_value = resp + result = _find_experiment_id( + mock_api, MagicMock(), MagicMock(), "my-experiment", None, B64_ID + ) + assert result == "exp-id" + + def test_name_ambiguous_across_standalone_and_dataset_backed_raises( + self, + ) -> None: + """No dataset provided, and the name matches both a standalone and a + dataset-associated experiment in the space: raises rather than + silently returning either one. + """ + resp = _make_paginated([]) + resp.experiments = [ + _item("my-experiment", "standalone-id", dataset_id=None), + _item("my-experiment", "dataset-backed-id", dataset_id="ds-1"), + ] + mock_api = MagicMock() + mock_api.list_experiments.return_value = resp + with pytest.raises(AmbiguousNameError, match="my-experiment"): + _find_experiment_id( + mock_api, + MagicMock(), + MagicMock(), + "my-experiment", + None, + B64_ID, + ) + def test_name_not_found_raises(self) -> None: resp = _make_paginated([]) resp.experiments = [_item("other-experiment")] mock_api = MagicMock() mock_api.list_experiments.return_value = resp with pytest.raises(NotFoundError, match="experiment"): - _find_experiment_id(mock_api, MagicMock(), "missing", B64_ID, None) + _find_experiment_id( + mock_api, MagicMock(), MagicMock(), "missing", B64_ID, None + ) def test_pagination(self) -> None: page1 = _make_paginated([], next_cursor="c") @@ -347,7 +414,9 @@ def test_pagination(self) -> None: mock_api = MagicMock() mock_api.list_experiments.side_effect = [page1, page2] assert ( - _find_experiment_id(mock_api, MagicMock(), "my-exp", B64_ID, None) + _find_experiment_id( + mock_api, MagicMock(), MagicMock(), "my-exp", B64_ID, None + ) == "exp-id" ) @@ -554,6 +623,104 @@ def test_pagination(self) -> None: ) +# --------------------------------------------------------------------------- +# _find_integration_id +# --------------------------------------------------------------------------- + + +def _wrapped_item(name: str, id: str = "some-id") -> MagicMock: + """Build a mock Integration oneOf wrapper with an inner actual_instance.""" + inner = MagicMock() + inner.name = name + inner.id = id + wrapper = MagicMock() + wrapper.actual_instance = inner + return wrapper + + +@pytest.mark.unit +class TestFindIntegrationId: + def test_base64_passthrough(self) -> None: + assert _find_integration_id(MagicMock(), B64_ID, "LLM", None) == B64_ID + + def test_base64_passthrough_without_type(self) -> None: + """An ID needs no type — it identifies the integration on its own.""" + mock_api = MagicMock() + assert _find_integration_id(mock_api, B64_ID, None, None) == B64_ID + mock_api.list_integrations.assert_not_called() + + def test_name_without_type_raises(self) -> None: + """A name is only unique per (account, type), so type is required.""" + mock_api = MagicMock() + with pytest.raises(NotFoundError, match="integration_type"): + _find_integration_id(mock_api, "my-integration", None, None) + mock_api.list_integrations.assert_not_called() + + def test_no_space_resolves_by_type(self) -> None: + """Without a space, resolution still lists by type and raises only + when the name is not found. + + Integration names are unique per ``(account, type)``, so ``space`` is + an optional visibility filter, not required to resolve a name. This + also guards against the pagination loop never terminating: a real + exhausted response has ``next_cursor=None``. + """ + resp = _make_paginated([]) + resp.integrations = [] + mock_api = MagicMock() + mock_api.list_integrations.return_value = resp + with pytest.raises(NotFoundError, match="integration"): + _find_integration_id(mock_api, "my-integration", "LLM", None) + # space is optional: the lookup proceeds with no space filter. + kwargs = mock_api.list_integrations.call_args.kwargs + assert kwargs["space_id"] is None + assert kwargs["space_name"] is None + + def test_name_resolved(self) -> None: + resp = _make_paginated([]) + resp.integrations = [_wrapped_item("my-integration", "int-id")] + mock_api = MagicMock() + mock_api.list_integrations.return_value = resp + result = _find_integration_id( + mock_api, "my-integration", "AGENT", B64_ID + ) + assert result == "int-id" + # type should be forwarded to the list endpoint + assert mock_api.list_integrations.call_args.kwargs["type"] == "AGENT" + + def test_name_not_found_raises(self) -> None: + resp = _make_paginated([]) + resp.integrations = [_wrapped_item("other-integration")] + mock_api = MagicMock() + mock_api.list_integrations.return_value = resp + with pytest.raises(NotFoundError, match="integration"): + _find_integration_id(mock_api, "missing", "LLM", B64_ID) + + def test_none_actual_instance_skipped(self) -> None: + empty = MagicMock() + empty.actual_instance = None + resp = _make_paginated([]) + resp.integrations = [empty, _wrapped_item("my-integration", "int-id")] + mock_api = MagicMock() + mock_api.list_integrations.return_value = resp + assert ( + _find_integration_id(mock_api, "my-integration", "LLM", B64_ID) + == "int-id" + ) + + def test_pagination(self) -> None: + page1 = _make_paginated([], next_cursor="c") + page1.integrations = [_wrapped_item("other")] + page2 = _make_paginated([]) + page2.integrations = [_wrapped_item("my-integration", "int-id")] + mock_api = MagicMock() + mock_api.list_integrations.side_effect = [page1, page2] + assert ( + _find_integration_id(mock_api, "my-integration", "LLM", B64_ID) + == "int-id" + ) + + # --------------------------------------------------------------------------- # _find_task_id # --------------------------------------------------------------------------- diff --git a/tests/unit/utils/test_unset.py b/tests/unit/utils/test_unset.py new file mode 100644 index 0000000..3ff17c6 --- /dev/null +++ b/tests/unit/utils/test_unset.py @@ -0,0 +1,14 @@ +"""Unit tests for the shared PATCH unset sentinel.""" + +from arize.utils.unset import _UNSET, UNSET, is_provided + + +def test_unset_is_not_provided() -> None: + """The shared sentinel represents an omitted PATCH argument.""" + assert isinstance(_UNSET, UNSET) + assert not is_provided(_UNSET) + + +def test_explicit_none_is_provided() -> None: + """None remains distinguishable from an omitted PATCH argument.""" + assert is_provided(None)