diff --git a/README.md b/README.md index fc72f69b2..d7f512ecf 100644 --- a/README.md +++ b/README.md @@ -811,6 +811,7 @@ Namespace | Resource | Operation | HTTP request | **MediaSets** | MediaSet | [**abort**](docs/v2/MediaSets/MediaSet.md#abort) | **POST** /v2/mediasets/{mediaSetRid}/transactions/{transactionId}/abort | **MediaSets** | MediaSet | [**commit**](docs/v2/MediaSets/MediaSet.md#commit) | **POST** /v2/mediasets/{mediaSetRid}/transactions/{transactionId}/commit | **MediaSets** | MediaSet | [**create**](docs/v2/MediaSets/MediaSet.md#create) | **POST** /v2/mediasets/{mediaSetRid}/transactions | +**MediaSets** | MediaSet | [**get**](docs/v2/MediaSets/MediaSet.md#get) | **GET** /v2/mediasets/{mediaSetRid} | **MediaSets** | MediaSet | [**get_result**](docs/v2/MediaSets/MediaSet.md#get_result) | **GET** /v2/mediasets/{mediaSetRid}/items/{mediaItemRid}/transformationJobs/{transformationJobId}/result | **MediaSets** | MediaSet | [**get_rid_by_path**](docs/v2/MediaSets/MediaSet.md#get_rid_by_path) | **GET** /v2/mediasets/{mediaSetRid}/items/getRidByPath | **MediaSets** | MediaSet | [**get_status**](docs/v2/MediaSets/MediaSet.md#get_status) | **GET** /v2/mediasets/{mediaSetRid}/items/{mediaItemRid}/transformationJobs/{transformationJobId} | @@ -819,6 +820,7 @@ Namespace | Resource | Operation | HTTP request | **MediaSets** | MediaSet | [**read**](docs/v2/MediaSets/MediaSet.md#read) | **GET** /v2/mediasets/{mediaSetRid}/items/{mediaItemRid}/content | **MediaSets** | MediaSet | [**read_original**](docs/v2/MediaSets/MediaSet.md#read_original) | **GET** /v2/mediasets/{mediaSetRid}/items/{mediaItemRid}/original | **MediaSets** | MediaSet | [**reference**](docs/v2/MediaSets/MediaSet.md#reference) | **GET** /v2/mediasets/{mediaSetRid}/items/{mediaItemRid}/reference | +**MediaSets** | MediaSet | [**register**](docs/v2/MediaSets/MediaSet.md#register) | **POST** /v2/mediasets/{mediaSetRid}/items/register | **MediaSets** | MediaSet | [**transform**](docs/v2/MediaSets/MediaSet.md#transform) | **POST** /v2/mediasets/{mediaSetRid}/items/{mediaItemRid}/transform | **MediaSets** | MediaSet | [**upload**](docs/v2/MediaSets/MediaSet.md#upload) | **POST** /v2/mediasets/{mediaSetRid}/items | **MediaSets** | MediaSet | [**upload_media**](docs/v2/MediaSets/MediaSet.md#upload_media) | **PUT** /v2/mediasets/media/upload | @@ -831,6 +833,7 @@ Namespace | Resource | Operation | HTTP request | **Ontologies** | Action | [**apply_batch**](docs/v2/Ontologies/Action.md#apply_batch) | **POST** /v2/ontologies/{ontology}/actions/{action}/applyBatch | **Ontologies** | ActionType | [**get**](docs/v2/Ontologies/ActionType.md#get) | **GET** /v2/ontologies/{ontology}/actionTypes/{actionType} | **Ontologies** | ActionType | [**get_by_rid**](docs/v2/Ontologies/ActionType.md#get_by_rid) | **GET** /v2/ontologies/{ontology}/actionTypes/byRid/{actionTypeRid} | +**Ontologies** | ActionType | [**get_by_rid_batch**](docs/v2/Ontologies/ActionType.md#get_by_rid_batch) | **POST** /v2/ontologies/{ontology}/actionTypes/getByRidBatch | **Ontologies** | ActionType | [**list**](docs/v2/Ontologies/ActionType.md#list) | **GET** /v2/ontologies/{ontology}/actionTypes | **Ontologies** | Attachment | [**get**](docs/v2/Ontologies/Attachment.md#get) | **GET** /v2/ontologies/attachments/{attachmentRid} | **Ontologies** | Attachment | [**read**](docs/v2/Ontologies/Attachment.md#read) | **GET** /v2/ontologies/attachments/{attachmentRid}/content | @@ -1414,6 +1417,7 @@ Namespace | Name | Import | **Core** | [VectorSimilarityFunctionValue](docs/v2/Core/models/VectorSimilarityFunctionValue.md) | `from foundry_sdk.v2.core.models import VectorSimilarityFunctionValue` | **Core** | [VectorType](docs/v2/Core/models/VectorType.md) | `from foundry_sdk.v2.core.models import VectorType` | **Core** | [VersionId](docs/v2/Core/models/VersionId.md) | `from foundry_sdk.v2.core.models import VersionId` | +**Core** | [VoidType](docs/v2/Core/models/VoidType.md) | `from foundry_sdk.v2.core.models import VoidType` | **Core** | [ZoneId](docs/v2/Core/models/ZoneId.md) | `from foundry_sdk.v2.core.models import ZoneId` | **DataHealth** | [AllowedColumnValuesCheckConfig](docs/v2/DataHealth/models/AllowedColumnValuesCheckConfig.md) | `from foundry_sdk.v2.data_health.models import AllowedColumnValuesCheckConfig` | **DataHealth** | [ApproximateUniquePercentageCheckConfig](docs/v2/DataHealth/models/ApproximateUniquePercentageCheckConfig.md) | `from foundry_sdk.v2.data_health.models import ApproximateUniquePercentageCheckConfig` | @@ -1761,6 +1765,7 @@ Namespace | Name | Import | **MediaSets** | [AudioTransformation](docs/v2/MediaSets/models/AudioTransformation.md) | `from foundry_sdk.v2.media_sets.models import AudioTransformation` | **MediaSets** | [AvailableEmbeddingModelIds](docs/v2/MediaSets/models/AvailableEmbeddingModelIds.md) | `from foundry_sdk.v2.media_sets.models import AvailableEmbeddingModelIds` | **MediaSets** | [BandInfo](docs/v2/MediaSets/models/BandInfo.md) | `from foundry_sdk.v2.media_sets.models import BandInfo` | +**MediaSets** | [BatchTransactionsTransactionPolicy](docs/v2/MediaSets/models/BatchTransactionsTransactionPolicy.md) | `from foundry_sdk.v2.media_sets.models import BatchTransactionsTransactionPolicy` | **MediaSets** | [BoundingBox](docs/v2/MediaSets/models/BoundingBox.md) | `from foundry_sdk.v2.media_sets.models import BoundingBox` | **MediaSets** | [BoundingBoxGeometry](docs/v2/MediaSets/models/BoundingBoxGeometry.md) | `from foundry_sdk.v2.media_sets.models import BoundingBoxGeometry` | **MediaSets** | [BranchName](docs/v2/MediaSets/models/BranchName.md) | `from foundry_sdk.v2.media_sets.models import BranchName` | @@ -1825,6 +1830,7 @@ Namespace | Name | Import | **MediaSets** | [GetEmailBodyOperation](docs/v2/MediaSets/models/GetEmailBodyOperation.md) | `from foundry_sdk.v2.media_sets.models import GetEmailBodyOperation` | **MediaSets** | [GetMediaItemInfoResponse](docs/v2/MediaSets/models/GetMediaItemInfoResponse.md) | `from foundry_sdk.v2.media_sets.models import GetMediaItemInfoResponse` | **MediaSets** | [GetMediaItemRidByPathResponse](docs/v2/MediaSets/models/GetMediaItemRidByPathResponse.md) | `from foundry_sdk.v2.media_sets.models import GetMediaItemRidByPathResponse` | +**MediaSets** | [GetMediaSetResponse](docs/v2/MediaSets/models/GetMediaSetResponse.md) | `from foundry_sdk.v2.media_sets.models import GetMediaSetResponse` | **MediaSets** | [GetPdfPageDimensionsOperation](docs/v2/MediaSets/models/GetPdfPageDimensionsOperation.md) | `from foundry_sdk.v2.media_sets.models import GetPdfPageDimensionsOperation` | **MediaSets** | [GetTimestampsForSceneFramesOperation](docs/v2/MediaSets/models/GetTimestampsForSceneFramesOperation.md) | `from foundry_sdk.v2.media_sets.models import GetTimestampsForSceneFramesOperation` | **MediaSets** | [GetTransformationJobStatusResponse](docs/v2/MediaSets/models/GetTransformationJobStatusResponse.md) | `from foundry_sdk.v2.media_sets.models import GetTransformationJobStatusResponse` | @@ -1859,6 +1865,7 @@ Namespace | Name | Import | **MediaSets** | [MediaAttribution](docs/v2/MediaSets/models/MediaAttribution.md) | `from foundry_sdk.v2.media_sets.models import MediaAttribution` | **MediaSets** | [MediaItemMetadata](docs/v2/MediaSets/models/MediaItemMetadata.md) | `from foundry_sdk.v2.media_sets.models import MediaItemMetadata` | **MediaSets** | [MediaItemXmlFormat](docs/v2/MediaSets/models/MediaItemXmlFormat.md) | `from foundry_sdk.v2.media_sets.models import MediaItemXmlFormat` | +**MediaSets** | [MediaSchema](docs/v2/MediaSets/models/MediaSchema.md) | `from foundry_sdk.v2.media_sets.models import MediaSchema` | **MediaSets** | [MkvVideoContainerFormat](docs/v2/MediaSets/models/MkvVideoContainerFormat.md) | `from foundry_sdk.v2.media_sets.models import MkvVideoContainerFormat` | **MediaSets** | [Modality](docs/v2/MediaSets/models/Modality.md) | `from foundry_sdk.v2.media_sets.models import Modality` | **MediaSets** | [Model3dDecodeFormat](docs/v2/MediaSets/models/Model3dDecodeFormat.md) | `from foundry_sdk.v2.media_sets.models import Model3dDecodeFormat` | @@ -1867,6 +1874,7 @@ Namespace | Name | Import | **MediaSets** | [MovVideoContainerFormat](docs/v2/MediaSets/models/MovVideoContainerFormat.md) | `from foundry_sdk.v2.media_sets.models import MovVideoContainerFormat` | **MediaSets** | [Mp3Format](docs/v2/MediaSets/models/Mp3Format.md) | `from foundry_sdk.v2.media_sets.models import Mp3Format` | **MediaSets** | [Mp4VideoContainerFormat](docs/v2/MediaSets/models/Mp4VideoContainerFormat.md) | `from foundry_sdk.v2.media_sets.models import Mp4VideoContainerFormat` | +**MediaSets** | [NoTransactionsTransactionPolicy](docs/v2/MediaSets/models/NoTransactionsTransactionPolicy.md) | `from foundry_sdk.v2.media_sets.models import NoTransactionsTransactionPolicy` | **MediaSets** | [NumberOfChannels](docs/v2/MediaSets/models/NumberOfChannels.md) | `from foundry_sdk.v2.media_sets.models import NumberOfChannels` | **MediaSets** | [OcrHocrOutputFormat](docs/v2/MediaSets/models/OcrHocrOutputFormat.md) | `from foundry_sdk.v2.media_sets.models import OcrHocrOutputFormat` | **MediaSets** | [OcrLanguage](docs/v2/MediaSets/models/OcrLanguage.md) | `from foundry_sdk.v2.media_sets.models import OcrLanguage` | @@ -1887,6 +1895,8 @@ Namespace | Name | Import | **MediaSets** | [PngFormat](docs/v2/MediaSets/models/PngFormat.md) | `from foundry_sdk.v2.media_sets.models import PngFormat` | **MediaSets** | [Pttml](docs/v2/MediaSets/models/Pttml.md) | `from foundry_sdk.v2.media_sets.models import Pttml` | **MediaSets** | [PutMediaItemResponse](docs/v2/MediaSets/models/PutMediaItemResponse.md) | `from foundry_sdk.v2.media_sets.models import PutMediaItemResponse` | +**MediaSets** | [RegisterMediaItemRequest](docs/v2/MediaSets/models/RegisterMediaItemRequest.md) | `from foundry_sdk.v2.media_sets.models import RegisterMediaItemRequest` | +**MediaSets** | [RegisterMediaItemResponse](docs/v2/MediaSets/models/RegisterMediaItemResponse.md) | `from foundry_sdk.v2.media_sets.models import RegisterMediaItemResponse` | **MediaSets** | [RenderImageLayerOperation](docs/v2/MediaSets/models/RenderImageLayerOperation.md) | `from foundry_sdk.v2.media_sets.models import RenderImageLayerOperation` | **MediaSets** | [RenderPageOperation](docs/v2/MediaSets/models/RenderPageOperation.md) | `from foundry_sdk.v2.media_sets.models import RenderPageOperation` | **MediaSets** | [RenderPageToFitBoundingBoxOperation](docs/v2/MediaSets/models/RenderPageToFitBoundingBoxOperation.md) | `from foundry_sdk.v2.media_sets.models import RenderPageToFitBoundingBoxOperation` | @@ -1908,6 +1918,7 @@ Namespace | Name | Import | **MediaSets** | [TrackedTransformationResponse](docs/v2/MediaSets/models/TrackedTransformationResponse.md) | `from foundry_sdk.v2.media_sets.models import TrackedTransformationResponse` | **MediaSets** | [TrackedTransformationSuccessfulResponse](docs/v2/MediaSets/models/TrackedTransformationSuccessfulResponse.md) | `from foundry_sdk.v2.media_sets.models import TrackedTransformationSuccessfulResponse` | **MediaSets** | [TransactionId](docs/v2/MediaSets/models/TransactionId.md) | `from foundry_sdk.v2.media_sets.models import TransactionId` | +**MediaSets** | [TransactionPolicy](docs/v2/MediaSets/models/TransactionPolicy.md) | `from foundry_sdk.v2.media_sets.models import TransactionPolicy` | **MediaSets** | [TranscodeOperation](docs/v2/MediaSets/models/TranscodeOperation.md) | `from foundry_sdk.v2.media_sets.models import TranscodeOperation` | **MediaSets** | [TranscribeJson](docs/v2/MediaSets/models/TranscribeJson.md) | `from foundry_sdk.v2.media_sets.models import TranscribeJson` | **MediaSets** | [TranscribeOperation](docs/v2/MediaSets/models/TranscribeOperation.md) | `from foundry_sdk.v2.media_sets.models import TranscribeOperation` | @@ -1961,7 +1972,6 @@ Namespace | Name | Import | **Models** | [ExperimentAuthoringSource](docs/v2/Models/models/ExperimentAuthoringSource.md) | `from foundry_sdk.v2.models.models import ExperimentAuthoringSource` | **Models** | [ExperimentBranch](docs/v2/Models/models/ExperimentBranch.md) | `from foundry_sdk.v2.models.models import ExperimentBranch` | **Models** | [ExperimentCodeWorkspaceSource](docs/v2/Models/models/ExperimentCodeWorkspaceSource.md) | `from foundry_sdk.v2.models.models import ExperimentCodeWorkspaceSource` | -**Models** | [ExperimentName](docs/v2/Models/models/ExperimentName.md) | `from foundry_sdk.v2.models.models import ExperimentName` | **Models** | [ExperimentRid](docs/v2/Models/models/ExperimentRid.md) | `from foundry_sdk.v2.models.models import ExperimentRid` | **Models** | [ExperimentSdkSource](docs/v2/Models/models/ExperimentSdkSource.md) | `from foundry_sdk.v2.models.models import ExperimentSdkSource` | **Models** | [ExperimentSource](docs/v2/Models/models/ExperimentSource.md) | `from foundry_sdk.v2.models.models import ExperimentSource` | @@ -2025,12 +2035,13 @@ Namespace | Name | Import | **Models** | [SearchExperimentsEqualsFilter](docs/v2/Models/models/SearchExperimentsEqualsFilter.md) | `from foundry_sdk.v2.models.models import SearchExperimentsEqualsFilter` | **Models** | [SearchExperimentsEqualsFilterField](docs/v2/Models/models/SearchExperimentsEqualsFilterField.md) | `from foundry_sdk.v2.models.models import SearchExperimentsEqualsFilterField` | **Models** | [SearchExperimentsFilter](docs/v2/Models/models/SearchExperimentsFilter.md) | `from foundry_sdk.v2.models.models import SearchExperimentsFilter` | -**Models** | [SearchExperimentsFilterOperator](docs/v2/Models/models/SearchExperimentsFilterOperator.md) | `from foundry_sdk.v2.models.models import SearchExperimentsFilterOperator` | **Models** | [SearchExperimentsNotFilter](docs/v2/Models/models/SearchExperimentsNotFilter.md) | `from foundry_sdk.v2.models.models import SearchExperimentsNotFilter` | +**Models** | [SearchExperimentsNumericFilterOperator](docs/v2/Models/models/SearchExperimentsNumericFilterOperator.md) | `from foundry_sdk.v2.models.models import SearchExperimentsNumericFilterOperator` | **Models** | [SearchExperimentsOrderBy](docs/v2/Models/models/SearchExperimentsOrderBy.md) | `from foundry_sdk.v2.models.models import SearchExperimentsOrderBy` | **Models** | [SearchExperimentsOrderByField](docs/v2/Models/models/SearchExperimentsOrderByField.md) | `from foundry_sdk.v2.models.models import SearchExperimentsOrderByField` | **Models** | [SearchExperimentsOrFilter](docs/v2/Models/models/SearchExperimentsOrFilter.md) | `from foundry_sdk.v2.models.models import SearchExperimentsOrFilter` | **Models** | [SearchExperimentsParameterFilter](docs/v2/Models/models/SearchExperimentsParameterFilter.md) | `from foundry_sdk.v2.models.models import SearchExperimentsParameterFilter` | +**Models** | [SearchExperimentsParameterFilterOperator](docs/v2/Models/models/SearchExperimentsParameterFilterOperator.md) | `from foundry_sdk.v2.models.models import SearchExperimentsParameterFilterOperator` | **Models** | [SearchExperimentsRequest](docs/v2/Models/models/SearchExperimentsRequest.md) | `from foundry_sdk.v2.models.models import SearchExperimentsRequest` | **Models** | [SearchExperimentsResponse](docs/v2/Models/models/SearchExperimentsResponse.md) | `from foundry_sdk.v2.models.models import SearchExperimentsResponse` | **Models** | [SearchExperimentsSeriesFilter](docs/v2/Models/models/SearchExperimentsSeriesFilter.md) | `from foundry_sdk.v2.models.models import SearchExperimentsSeriesFilter` | @@ -2223,6 +2234,9 @@ Namespace | Name | Import | **Ontologies** | [GeoShapeV2Query](docs/v2/Ontologies/models/GeoShapeV2Query.md) | `from foundry_sdk.v2.ontologies.models import GeoShapeV2Query` | **Ontologies** | [GeotemporalSeriesEntry](docs/v2/Ontologies/models/GeotemporalSeriesEntry.md) | `from foundry_sdk.v2.ontologies.models import GeotemporalSeriesEntry` | **Ontologies** | [GeotimeSeriesValue](docs/v2/Ontologies/models/GeotimeSeriesValue.md) | `from foundry_sdk.v2.ontologies.models import GeotimeSeriesValue` | +**Ontologies** | [GetActionTypeByRidBatchRequest](docs/v2/Ontologies/models/GetActionTypeByRidBatchRequest.md) | `from foundry_sdk.v2.ontologies.models import GetActionTypeByRidBatchRequest` | +**Ontologies** | [GetActionTypeByRidBatchRequestElement](docs/v2/Ontologies/models/GetActionTypeByRidBatchRequestElement.md) | `from foundry_sdk.v2.ontologies.models import GetActionTypeByRidBatchRequestElement` | +**Ontologies** | [GetActionTypeByRidBatchResponse](docs/v2/Ontologies/models/GetActionTypeByRidBatchResponse.md) | `from foundry_sdk.v2.ontologies.models import GetActionTypeByRidBatchResponse` | **Ontologies** | [GetSelectedPropertyOperation](docs/v2/Ontologies/models/GetSelectedPropertyOperation.md) | `from foundry_sdk.v2.ontologies.models import GetSelectedPropertyOperation` | **Ontologies** | [GreatestPropertyExpression](docs/v2/Ontologies/models/GreatestPropertyExpression.md) | `from foundry_sdk.v2.ontologies.models import GreatestPropertyExpression` | **Ontologies** | [GroupMemberConstraint](docs/v2/Ontologies/models/GroupMemberConstraint.md) | `from foundry_sdk.v2.ontologies.models import GroupMemberConstraint` | @@ -2700,7 +2714,6 @@ Namespace | Name | Import | **SqlQueries** | [MapParameterKey](docs/v2/SqlQueries/models/MapParameterKey.md) | `from foundry_sdk.v2.sql_queries.models import MapParameterKey` | **SqlQueries** | [NamedParameterMapping](docs/v2/SqlQueries/models/NamedParameterMapping.md) | `from foundry_sdk.v2.sql_queries.models import NamedParameterMapping` | **SqlQueries** | [ParameterAnyValue](docs/v2/SqlQueries/models/ParameterAnyValue.md) | `from foundry_sdk.v2.sql_queries.models import ParameterAnyValue` | -**SqlQueries** | [ParameterBinaryValue](docs/v2/SqlQueries/models/ParameterBinaryValue.md) | `from foundry_sdk.v2.sql_queries.models import ParameterBinaryValue` | **SqlQueries** | [ParameterBooleanValue](docs/v2/SqlQueries/models/ParameterBooleanValue.md) | `from foundry_sdk.v2.sql_queries.models import ParameterBooleanValue` | **SqlQueries** | [ParameterDateValue](docs/v2/SqlQueries/models/ParameterDateValue.md) | `from foundry_sdk.v2.sql_queries.models import ParameterDateValue` | **SqlQueries** | [ParameterDecimalValue](docs/v2/SqlQueries/models/ParameterDecimalValue.md) | `from foundry_sdk.v2.sql_queries.models import ParameterDecimalValue` | @@ -2824,6 +2837,7 @@ Namespace | Name | Import | **Core** | [UnsupportedType](docs/v1/Core/models/UnsupportedType.md) | `from foundry_sdk.v1.core.models import UnsupportedType` | **Core** | [UnsupportedTypeParamKey](docs/v1/Core/models/UnsupportedTypeParamKey.md) | `from foundry_sdk.v1.core.models import UnsupportedTypeParamKey` | **Core** | [UnsupportedTypeParamValue](docs/v1/Core/models/UnsupportedTypeParamValue.md) | `from foundry_sdk.v1.core.models import UnsupportedTypeParamValue` | +**Core** | [VoidType](docs/v1/Core/models/VoidType.md) | `from foundry_sdk.v1.core.models import VoidType` | **Datasets** | [Branch](docs/v1/Datasets/models/Branch.md) | `from foundry_sdk.v1.datasets.models import Branch` | **Datasets** | [BranchId](docs/v1/Datasets/models/BranchId.md) | `from foundry_sdk.v1.datasets.models import BranchId` | **Datasets** | [CreateBranchRequest](docs/v1/Datasets/models/CreateBranchRequest.md) | `from foundry_sdk.v1.datasets.models import CreateBranchRequest` | @@ -3392,6 +3406,7 @@ Namespace | Name | Import | **Models** | InferenceFailure | `from foundry_sdk.v2.models.errors import InferenceFailure` | **Models** | InferenceInvalidInput | `from foundry_sdk.v2.models.errors import InferenceInvalidInput` | **Models** | InferenceTimeout | `from foundry_sdk.v2.models.errors import InferenceTimeout` | +**Models** | InvalidExperimentSearchFilter | `from foundry_sdk.v2.models.errors import InvalidExperimentSearchFilter` | **Models** | InvalidModelApi | `from foundry_sdk.v2.models.errors import InvalidModelApi` | **Models** | InvalidModelStudioCreateRequest | `from foundry_sdk.v2.models.errors import InvalidModelStudioCreateRequest` | **Models** | JsonExperimentArtifactTablePermissionDenied | `from foundry_sdk.v2.models.errors import JsonExperimentArtifactTablePermissionDenied` | diff --git a/docs-snippets-npm/package.json b/docs-snippets-npm/package.json index 927f13741..5d7229e43 100644 --- a/docs-snippets-npm/package.json +++ b/docs-snippets-npm/package.json @@ -24,7 +24,7 @@ "sls": { "dependencies": { "com.palantir.foundry.api:api-gateway": { - "minVersion": "1.1485.0", + "minVersion": "1.1494.0", "maxVersion": "1.x.x", "optional": false } diff --git a/docs-snippets-npm/src/index.ts b/docs-snippets-npm/src/index.ts index 4c4a2e332..ae4492ae6 100644 --- a/docs-snippets-npm/src/index.ts +++ b/docs-snippets-npm/src/index.ts @@ -1060,7 +1060,7 @@ export const PYTHON_PLATFORM_SNIPPETS: SdkSnippets str: + """Get the Foundry hostname from the current execution context. + + Args: + preview: Must be set to True to use this beta feature. + + Returns: + The Foundry API gateway base URL. + + Raises: + ValueError: If preview is not set to True. + RuntimeError: If the Foundry API gateway base URL is not available in the current context. + """ + if not preview: + raise ValueError( + "get_api_gateway_base_url() is in beta. " + "Please set the preview parameter to True to use it." + ) + hostname = HOSTNAME_VAR.get() + if hostname is None: + raise RuntimeError("Foundry API gateway base URL is not available in the current context.") + return hostname + + +def get_foundry_token(*, preview: bool = False) -> str: + """Get the Foundry token from the current execution context. + + Args: + preview: Must be set to True to use this beta feature. + + Returns: + The Foundry token. + + Raises: + ValueError: If preview is not set to True. + RuntimeError: If the Foundry token is not available in the current context. + """ + if not preview: + raise ValueError( + "get_foundry_token() is in beta. " "Please set the preview parameter to True to use it." + ) + token = TOKEN_VAR.get() + if token is None: + raise RuntimeError("Foundry token is not available in the current context.") + return token + + +def get_openai_base_url(*, preview: bool = False) -> str: + """Get the OpenAI proxy base URL for the current Foundry environment. + + Args: + preview: Must be set to True to use this beta feature. + + Returns: + The OpenAI proxy base URL. + + Raises: + ValueError: If preview is not set to True. + RuntimeError: If the Foundry API gateway base URL is not available in the current context. + """ + if not preview: + raise ValueError( + "get_openai_base_url() is in beta. " + "Please set the preview parameter to True to use it." + ) + hostname = _get_api_gateway_base_url(preview=True) + return f"https://{hostname}/api/v2/llm/proxy/openai/v1" + + +def get_anthropic_base_url(*, preview: bool = False) -> str: + """Get the Anthropic proxy base URL for the current Foundry environment. + + Args: + preview: Must be set to True to use this beta feature. + + Returns: + The Anthropic proxy base URL. + + Raises: + ValueError: If preview is not set to True. + RuntimeError: If the Foundry API gateway base URL is not available in the current context. + """ + if not preview: + raise ValueError( + "get_anthropic_base_url() is in beta. " + "Please set the preview parameter to True to use it." + ) + hostname = _get_api_gateway_base_url(preview=True) + return f"https://{hostname}/api/v2/llm/proxy/anthropic" + + +def get_http_client(*, preview: bool = False, config: Optional[Config] = None) -> HttpClient: + """Get an HTTP client configured for the current Foundry environment. + + Args: + preview: Must be set to True to use this beta feature. + config: Optional configuration for the HTTP client. + + Returns: + An HttpClient instance configured with the Foundry hostname and authentication. + + Raises: + ValueError: If preview is not set to True. + RuntimeError: If the Foundry API gateway base URL or token is not available in the current context. + """ + if not preview: + raise ValueError( + "get_http_client() is in beta. " "Please set the preview parameter to True to use it." + ) + hostname = _get_api_gateway_base_url(preview=True) + token = get_foundry_token(preview=True) + + # Merge auth header with any user-provided headers + auth_header = {"Authorization": f"Bearer {token}"} + if config is None: + config = Config(default_headers=auth_header) + else: + merged_headers = {**auth_header, **(config.default_headers or {})} + config = replace(config, default_headers=merged_headers) + + return HttpClient(hostname=hostname, config=config) diff --git a/foundry_sdk/v2/media_sets/media_set.py b/foundry_sdk/v2/media_sets/media_set.py index 11d774e1c..ada5b4923 100644 --- a/foundry_sdk/v2/media_sets/media_set.py +++ b/foundry_sdk/v2/media_sets/media_set.py @@ -243,6 +243,51 @@ def create( ), ) + @core.maybe_ignore_preview + @pydantic.validate_call + @errors.handle_unexpected + def get( + self, + media_set_rid: core_models.MediaSetRid, + *, + preview: typing.Optional[core_models.PreviewMode] = None, + request_timeout: typing.Optional[core.Timeout] = None, + _sdk_internal: core.SdkInternal = {}, + ) -> media_sets_models.GetMediaSetResponse: + """ + Gets information about the media set. + + :param media_set_rid: + :type media_set_rid: MediaSetRid + :param preview: A boolean flag that, when set to true, enables the use of beta features in preview mode. + :type preview: Optional[PreviewMode] + :param request_timeout: timeout setting for this request in seconds. + :type request_timeout: Optional[int] + :return: Returns the result object. + :rtype: media_sets_models.GetMediaSetResponse + """ + + return self._api_client.call_api( + core.RequestInfo( + method="GET", + resource_path="/v2/mediasets/{mediaSetRid}", + query_params={ + "preview": preview, + }, + path_params={ + "mediaSetRid": media_set_rid, + }, + header_params={ + "Accept": "application/json", + }, + body=None, + response_type=media_sets_models.GetMediaSetResponse, + request_timeout=request_timeout, + throwable_errors={}, + response_mode=_sdk_internal.get("response_mode"), + ), + ) + @core.maybe_ignore_preview @pydantic.validate_call @errors.handle_unexpected @@ -685,6 +730,75 @@ def reference( ), ) + @core.maybe_ignore_preview + @pydantic.validate_call + @errors.handle_unexpected + def register( + self, + media_set_rid: core_models.MediaSetRid, + *, + physical_item_name: str, + branch_name: typing.Optional[media_sets_models.BranchName] = None, + media_item_path: typing.Optional[core_models.MediaItemPath] = None, + preview: typing.Optional[core_models.PreviewMode] = None, + transaction_id: typing.Optional[media_sets_models.TransactionId] = None, + view_rid: typing.Optional[core_models.MediaSetViewRid] = None, + request_timeout: typing.Optional[core.Timeout] = None, + _sdk_internal: core.SdkInternal = {}, + ) -> media_sets_models.RegisterMediaItemResponse: + """ + Registers a media item that currently resides in a federated media store. Registration will validate the item + against the media set's schema and perform initial metadata extraction. + This endpoint is only applicable for federated media sets. + + :param media_set_rid: + :type media_set_rid: MediaSetRid + :param physical_item_name: The relative path within the federated media store where the media item exists. + :type physical_item_name: str + :param branch_name: Specifies the specific branch by name to which this media item will be registered. + :type branch_name: Optional[BranchName] + :param media_item_path: + :type media_item_path: Optional[MediaItemPath] + :param preview: A boolean flag that, when set to true, enables the use of beta features in preview mode. + :type preview: Optional[PreviewMode] + :param transaction_id: The id of the transaction associated with this request. Required for transactional media sets. + :type transaction_id: Optional[TransactionId] + :param view_rid: Specifies the specific view by rid to which this media item will be registered. + :type view_rid: Optional[MediaSetViewRid] + :param request_timeout: timeout setting for this request in seconds. + :type request_timeout: Optional[int] + :return: Returns the result object. + :rtype: media_sets_models.RegisterMediaItemResponse + """ + + return self._api_client.call_api( + core.RequestInfo( + method="POST", + resource_path="/v2/mediasets/{mediaSetRid}/items/register", + query_params={ + "branchName": branch_name, + "preview": preview, + "transactionId": transaction_id, + "viewRid": view_rid, + }, + path_params={ + "mediaSetRid": media_set_rid, + }, + header_params={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + body=media_sets_models.RegisterMediaItemRequest( + physical_item_name=physical_item_name, + media_item_path=media_item_path, + ), + response_type=media_sets_models.RegisterMediaItemResponse, + request_timeout=request_timeout, + throwable_errors={}, + response_mode=_sdk_internal.get("response_mode"), + ), + ) + @core.maybe_ignore_preview @pydantic.validate_call @errors.handle_unexpected @@ -942,6 +1056,7 @@ def abort(_: None): ... def calculate(_: media_sets_models.TrackedTransformationResponse): ... def commit(_: None): ... def create(_: media_sets_models.TransactionId): ... + def get(_: media_sets_models.GetMediaSetResponse): ... def get_result(_: bytes): ... def get_rid_by_path(_: media_sets_models.GetMediaItemRidByPathResponse): ... def get_status(_: media_sets_models.GetTransformationJobStatusResponse): ... @@ -950,6 +1065,7 @@ def metadata(_: media_sets_models.MediaItemMetadata): ... def read(_: bytes): ... def read_original(_: bytes): ... def reference(_: core_models.MediaReference): ... + def register(_: media_sets_models.RegisterMediaItemResponse): ... def retrieve(_: bytes): ... def transform(_: media_sets_models.TransformMediaItemResponse): ... def upload(_: media_sets_models.PutMediaItemResponse): ... @@ -959,6 +1075,7 @@ def upload_media(_: core_models.MediaReference): ... self.calculate = core.with_raw_response(calculate, client.calculate) self.commit = core.with_raw_response(commit, client.commit) self.create = core.with_raw_response(create, client.create) + self.get = core.with_raw_response(get, client.get) self.get_result = core.with_raw_response(get_result, client.get_result) self.get_rid_by_path = core.with_raw_response(get_rid_by_path, client.get_rid_by_path) self.get_status = core.with_raw_response(get_status, client.get_status) @@ -967,6 +1084,7 @@ def upload_media(_: core_models.MediaReference): ... self.read = core.with_raw_response(read, client.read) self.read_original = core.with_raw_response(read_original, client.read_original) self.reference = core.with_raw_response(reference, client.reference) + self.register = core.with_raw_response(register, client.register) self.retrieve = core.with_raw_response(retrieve, client.retrieve) self.transform = core.with_raw_response(transform, client.transform) self.upload = core.with_raw_response(upload, client.upload) @@ -977,6 +1095,7 @@ class _MediaSetClientStreaming: def __init__(self, client: MediaSetClient) -> None: def calculate(_: media_sets_models.TrackedTransformationResponse): ... def create(_: media_sets_models.TransactionId): ... + def get(_: media_sets_models.GetMediaSetResponse): ... def get_result(_: bytes): ... def get_rid_by_path(_: media_sets_models.GetMediaItemRidByPathResponse): ... def get_status(_: media_sets_models.GetTransformationJobStatusResponse): ... @@ -985,6 +1104,7 @@ def metadata(_: media_sets_models.MediaItemMetadata): ... def read(_: bytes): ... def read_original(_: bytes): ... def reference(_: core_models.MediaReference): ... + def register(_: media_sets_models.RegisterMediaItemResponse): ... def retrieve(_: bytes): ... def transform(_: media_sets_models.TransformMediaItemResponse): ... def upload(_: media_sets_models.PutMediaItemResponse): ... @@ -992,6 +1112,7 @@ def upload_media(_: core_models.MediaReference): ... self.calculate = core.with_streaming_response(calculate, client.calculate) self.create = core.with_streaming_response(create, client.create) + self.get = core.with_streaming_response(get, client.get) self.get_result = core.with_streaming_response(get_result, client.get_result) self.get_rid_by_path = core.with_streaming_response(get_rid_by_path, client.get_rid_by_path) self.get_status = core.with_streaming_response(get_status, client.get_status) @@ -1000,6 +1121,7 @@ def upload_media(_: core_models.MediaReference): ... self.read = core.with_streaming_response(read, client.read) self.read_original = core.with_streaming_response(read_original, client.read_original) self.reference = core.with_streaming_response(reference, client.reference) + self.register = core.with_streaming_response(register, client.register) self.retrieve = core.with_streaming_response(retrieve, client.retrieve) self.transform = core.with_streaming_response(transform, client.transform) self.upload = core.with_streaming_response(upload, client.upload) @@ -1225,6 +1347,51 @@ def create( ), ) + @core.maybe_ignore_preview + @pydantic.validate_call + @errors.handle_unexpected + def get( + self, + media_set_rid: core_models.MediaSetRid, + *, + preview: typing.Optional[core_models.PreviewMode] = None, + request_timeout: typing.Optional[core.Timeout] = None, + _sdk_internal: core.SdkInternal = {}, + ) -> typing.Awaitable[media_sets_models.GetMediaSetResponse]: + """ + Gets information about the media set. + + :param media_set_rid: + :type media_set_rid: MediaSetRid + :param preview: A boolean flag that, when set to true, enables the use of beta features in preview mode. + :type preview: Optional[PreviewMode] + :param request_timeout: timeout setting for this request in seconds. + :type request_timeout: Optional[int] + :return: Returns the result object. + :rtype: typing.Awaitable[media_sets_models.GetMediaSetResponse] + """ + + return self._api_client.call_api( + core.RequestInfo( + method="GET", + resource_path="/v2/mediasets/{mediaSetRid}", + query_params={ + "preview": preview, + }, + path_params={ + "mediaSetRid": media_set_rid, + }, + header_params={ + "Accept": "application/json", + }, + body=None, + response_type=media_sets_models.GetMediaSetResponse, + request_timeout=request_timeout, + throwable_errors={}, + response_mode=_sdk_internal.get("response_mode"), + ), + ) + @core.maybe_ignore_preview @pydantic.validate_call @errors.handle_unexpected @@ -1667,6 +1834,75 @@ def reference( ), ) + @core.maybe_ignore_preview + @pydantic.validate_call + @errors.handle_unexpected + def register( + self, + media_set_rid: core_models.MediaSetRid, + *, + physical_item_name: str, + branch_name: typing.Optional[media_sets_models.BranchName] = None, + media_item_path: typing.Optional[core_models.MediaItemPath] = None, + preview: typing.Optional[core_models.PreviewMode] = None, + transaction_id: typing.Optional[media_sets_models.TransactionId] = None, + view_rid: typing.Optional[core_models.MediaSetViewRid] = None, + request_timeout: typing.Optional[core.Timeout] = None, + _sdk_internal: core.SdkInternal = {}, + ) -> typing.Awaitable[media_sets_models.RegisterMediaItemResponse]: + """ + Registers a media item that currently resides in a federated media store. Registration will validate the item + against the media set's schema and perform initial metadata extraction. + This endpoint is only applicable for federated media sets. + + :param media_set_rid: + :type media_set_rid: MediaSetRid + :param physical_item_name: The relative path within the federated media store where the media item exists. + :type physical_item_name: str + :param branch_name: Specifies the specific branch by name to which this media item will be registered. + :type branch_name: Optional[BranchName] + :param media_item_path: + :type media_item_path: Optional[MediaItemPath] + :param preview: A boolean flag that, when set to true, enables the use of beta features in preview mode. + :type preview: Optional[PreviewMode] + :param transaction_id: The id of the transaction associated with this request. Required for transactional media sets. + :type transaction_id: Optional[TransactionId] + :param view_rid: Specifies the specific view by rid to which this media item will be registered. + :type view_rid: Optional[MediaSetViewRid] + :param request_timeout: timeout setting for this request in seconds. + :type request_timeout: Optional[int] + :return: Returns the result object. + :rtype: typing.Awaitable[media_sets_models.RegisterMediaItemResponse] + """ + + return self._api_client.call_api( + core.RequestInfo( + method="POST", + resource_path="/v2/mediasets/{mediaSetRid}/items/register", + query_params={ + "branchName": branch_name, + "preview": preview, + "transactionId": transaction_id, + "viewRid": view_rid, + }, + path_params={ + "mediaSetRid": media_set_rid, + }, + header_params={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + body=media_sets_models.RegisterMediaItemRequest( + physical_item_name=physical_item_name, + media_item_path=media_item_path, + ), + response_type=media_sets_models.RegisterMediaItemResponse, + request_timeout=request_timeout, + throwable_errors={}, + response_mode=_sdk_internal.get("response_mode"), + ), + ) + @core.maybe_ignore_preview @pydantic.validate_call @errors.handle_unexpected @@ -1924,6 +2160,7 @@ def abort(_: None): ... def calculate(_: media_sets_models.TrackedTransformationResponse): ... def commit(_: None): ... def create(_: media_sets_models.TransactionId): ... + def get(_: media_sets_models.GetMediaSetResponse): ... def get_result(_: bytes): ... def get_rid_by_path(_: media_sets_models.GetMediaItemRidByPathResponse): ... def get_status(_: media_sets_models.GetTransformationJobStatusResponse): ... @@ -1932,6 +2169,7 @@ def metadata(_: media_sets_models.MediaItemMetadata): ... def read(_: bytes): ... def read_original(_: bytes): ... def reference(_: core_models.MediaReference): ... + def register(_: media_sets_models.RegisterMediaItemResponse): ... def retrieve(_: bytes): ... def transform(_: media_sets_models.TransformMediaItemResponse): ... def upload(_: media_sets_models.PutMediaItemResponse): ... @@ -1941,6 +2179,7 @@ def upload_media(_: core_models.MediaReference): ... self.calculate = core.async_with_raw_response(calculate, client.calculate) self.commit = core.async_with_raw_response(commit, client.commit) self.create = core.async_with_raw_response(create, client.create) + self.get = core.async_with_raw_response(get, client.get) self.get_result = core.async_with_raw_response(get_result, client.get_result) self.get_rid_by_path = core.async_with_raw_response(get_rid_by_path, client.get_rid_by_path) self.get_status = core.async_with_raw_response(get_status, client.get_status) @@ -1949,6 +2188,7 @@ def upload_media(_: core_models.MediaReference): ... self.read = core.async_with_raw_response(read, client.read) self.read_original = core.async_with_raw_response(read_original, client.read_original) self.reference = core.async_with_raw_response(reference, client.reference) + self.register = core.async_with_raw_response(register, client.register) self.retrieve = core.async_with_raw_response(retrieve, client.retrieve) self.transform = core.async_with_raw_response(transform, client.transform) self.upload = core.async_with_raw_response(upload, client.upload) @@ -1959,6 +2199,7 @@ class _AsyncMediaSetClientStreaming: def __init__(self, client: AsyncMediaSetClient) -> None: def calculate(_: media_sets_models.TrackedTransformationResponse): ... def create(_: media_sets_models.TransactionId): ... + def get(_: media_sets_models.GetMediaSetResponse): ... def get_result(_: bytes): ... def get_rid_by_path(_: media_sets_models.GetMediaItemRidByPathResponse): ... def get_status(_: media_sets_models.GetTransformationJobStatusResponse): ... @@ -1967,6 +2208,7 @@ def metadata(_: media_sets_models.MediaItemMetadata): ... def read(_: bytes): ... def read_original(_: bytes): ... def reference(_: core_models.MediaReference): ... + def register(_: media_sets_models.RegisterMediaItemResponse): ... def retrieve(_: bytes): ... def transform(_: media_sets_models.TransformMediaItemResponse): ... def upload(_: media_sets_models.PutMediaItemResponse): ... @@ -1974,6 +2216,7 @@ def upload_media(_: core_models.MediaReference): ... self.calculate = core.async_with_streaming_response(calculate, client.calculate) self.create = core.async_with_streaming_response(create, client.create) + self.get = core.async_with_streaming_response(get, client.get) self.get_result = core.async_with_streaming_response(get_result, client.get_result) self.get_rid_by_path = core.async_with_streaming_response( get_rid_by_path, client.get_rid_by_path @@ -1984,6 +2227,7 @@ def upload_media(_: core_models.MediaReference): ... self.read = core.async_with_streaming_response(read, client.read) self.read_original = core.async_with_streaming_response(read_original, client.read_original) self.reference = core.async_with_streaming_response(reference, client.reference) + self.register = core.async_with_streaming_response(register, client.register) self.retrieve = core.async_with_streaming_response(retrieve, client.retrieve) self.transform = core.async_with_streaming_response(transform, client.transform) self.upload = core.async_with_streaming_response(upload, client.upload) diff --git a/foundry_sdk/v2/media_sets/models.py b/foundry_sdk/v2/media_sets/models.py index f9c7b6e0b..145c0600f 100644 --- a/foundry_sdk/v2/media_sets/models.py +++ b/foundry_sdk/v2/media_sets/models.py @@ -157,6 +157,15 @@ class BandInfo(core.ModelBase): unit_interpretation: typing.Optional[UnitInterpretation] = pydantic.Field(alias=str("unitInterpretation"), default=None) # type: ignore[literal-required] +class BatchTransactionsTransactionPolicy(core.ModelBase): + """ + All writes must be part of a transaction. Transactions are branch-scoped and created by calling + create transaction. Writes are not visible until commit transaction is called. + """ + + type: typing.Literal["batchTransactions"] = "batchTransactions" + + class BoundingBox(core.ModelBase): """A rectangular bounding box for annotations.""" @@ -759,6 +768,17 @@ class GetMediaItemRidByPathResponse(core.ModelBase): media_item_rid: typing.Optional[core_models.MediaItemRid] = pydantic.Field(alias=str("mediaItemRid"), default=None) # type: ignore[literal-required] +class GetMediaSetResponse(core.ModelBase): + """Information about a media set.""" + + rid: core_models.MediaSetRid + media_schema: MediaSchema = pydantic.Field(alias=str("mediaSchema")) # type: ignore[literal-required] + default_branch_name: BranchName = pydantic.Field(alias=str("defaultBranchName")) # type: ignore[literal-required] + transaction_policy: TransactionPolicy = pydantic.Field(alias=str("transactionPolicy")) # type: ignore[literal-required] + paths_required: bool = pydantic.Field(alias=str("pathsRequired")) # type: ignore[literal-required] + """Whether media items in this media set require paths.""" + + class GetPdfPageDimensionsOperation(core.ModelBase): """Returns the dimensions of each page in a PDF document as JSON (in points).""" @@ -1048,6 +1068,20 @@ class MediaAttribution(core.ModelBase): """Format of the media item attempted to be decoded based on the XML structure.""" +MediaSchema = typing.Literal[ + "AUDIO", + "DICOM", + "DOCUMENT", + "IMAGERY", + "MODEL_3D", + "MULTIMODAL", + "SPREADSHEET", + "VIDEO", + "EMAIL", +] +"""The schema type of a media set, indicating what type of media items it can contain.""" + + class MkvVideoContainerFormat(core.ModelBase): """MKV (Matroska) video container format.""" @@ -1187,6 +1221,15 @@ class Mp4VideoContainerFormat(core.ModelBase): type: typing.Literal["mp4"] = "mp4" +class NoTransactionsTransactionPolicy(core.ModelBase): + """ + Writes are not part of a transaction and are immediately visible. + Calls to create transaction or commit transaction will error. + """ + + type: typing.Literal["noTransactions"] = "noTransactions" + + class NumberOfChannels(core.ModelBase): """Specifies the number of audio channels. Defaults to 2 (stereo).""" @@ -1480,6 +1523,22 @@ class PutMediaItemResponse(core.ModelBase): media_item_rid: core_models.MediaItemRid = pydantic.Field(alias=str("mediaItemRid")) # type: ignore[literal-required] +class RegisterMediaItemRequest(core.ModelBase): + """Request to register a media item from a federated store.""" + + physical_item_name: str = pydantic.Field(alias=str("physicalItemName")) # type: ignore[literal-required] + """The relative path within the federated media store where the media item exists.""" + + media_item_path: typing.Optional[core_models.MediaItemPath] = pydantic.Field(alias=str("mediaItemPath"), default=None) # type: ignore[literal-required] + + +class RegisterMediaItemResponse(core.ModelBase): + """Response after successfully registering a media item.""" + + media_item_rid: core_models.MediaItemRid = pydantic.Field(alias=str("mediaItemRid")) # type: ignore[literal-required] + media_type: core_models.MediaType = pydantic.Field(alias=str("mediaType")) # type: ignore[literal-required] + + class RenderImageLayerOperation(core.ModelBase): """ Renders a frame of a DICOM file as an image. @@ -1687,6 +1746,13 @@ class TrackedTransformationSuccessfulResponse(core.ModelBase): """An identifier which represents a transaction on a media set.""" +TransactionPolicy = typing_extensions.Annotated[ + typing.Union["BatchTransactionsTransactionPolicy", "NoTransactionsTransactionPolicy"], + pydantic.Field(discriminator="type"), +] +"""The transaction policy for a media set, determining how writes are handled.""" + + class TranscodeOperation(core.ModelBase): """Encodes video to the specified format.""" @@ -2224,6 +2290,7 @@ class WebpFormat(core.ModelBase): core.resolve_forward_references(OcrLanguageOrScript, globalns=globals(), localns=locals()) core.resolve_forward_references(OcrOutputFormat, globalns=globals(), localns=locals()) core.resolve_forward_references(TrackedTransformationResponse, globalns=globals(), localns=locals()) +core.resolve_forward_references(TransactionPolicy, globalns=globals(), localns=locals()) core.resolve_forward_references(TranscribeTextEncodeFormat, globalns=globals(), localns=locals()) core.resolve_forward_references(Transformation, globalns=globals(), localns=locals()) core.resolve_forward_references(VideoEncodeFormat, globalns=globals(), localns=locals()) @@ -2249,6 +2316,7 @@ class WebpFormat(core.ModelBase): "AudioTransformation", "AvailableEmbeddingModelIds", "BandInfo", + "BatchTransactionsTransactionPolicy", "BoundingBox", "BoundingBoxGeometry", "BranchName", @@ -2313,6 +2381,7 @@ class WebpFormat(core.ModelBase): "GetEmailBodyOperation", "GetMediaItemInfoResponse", "GetMediaItemRidByPathResponse", + "GetMediaSetResponse", "GetPdfPageDimensionsOperation", "GetTimestampsForSceneFramesOperation", "GetTransformationJobStatusResponse", @@ -2347,6 +2416,7 @@ class WebpFormat(core.ModelBase): "MediaAttribution", "MediaItemMetadata", "MediaItemXmlFormat", + "MediaSchema", "MkvVideoContainerFormat", "Modality", "Model3dDecodeFormat", @@ -2355,6 +2425,7 @@ class WebpFormat(core.ModelBase): "MovVideoContainerFormat", "Mp3Format", "Mp4VideoContainerFormat", + "NoTransactionsTransactionPolicy", "NumberOfChannels", "OcrHocrOutputFormat", "OcrLanguage", @@ -2375,6 +2446,8 @@ class WebpFormat(core.ModelBase): "PngFormat", "Pttml", "PutMediaItemResponse", + "RegisterMediaItemRequest", + "RegisterMediaItemResponse", "RenderImageLayerOperation", "RenderPageOperation", "RenderPageToFitBoundingBoxOperation", @@ -2396,6 +2469,7 @@ class WebpFormat(core.ModelBase): "TrackedTransformationResponse", "TrackedTransformationSuccessfulResponse", "TransactionId", + "TransactionPolicy", "TranscodeOperation", "TranscribeJson", "TranscribeOperation", diff --git a/foundry_sdk/v2/models/errors.py b/foundry_sdk/v2/models/errors.py index f88596c06..1bbb20243 100644 --- a/foundry_sdk/v2/models/errors.py +++ b/foundry_sdk/v2/models/errors.py @@ -218,6 +218,25 @@ class InferenceTimeout(errors.InternalServerError): error_instance_id: str +class InvalidExperimentSearchFilterParameters(typing_extensions.TypedDict): + """ + The search filter is invalid. This can occur when using an unsupported operator and value type + combination in a parameter filter, filtering by an unsupported status, or providing a malformed filter. + """ + + __pydantic_config__ = {"extra": "allow"} # type: ignore + + reason: str + """A human-readable description of why the filter is invalid.""" + + +@dataclass +class InvalidExperimentSearchFilter(errors.BadRequestError): + name: typing.Literal["InvalidExperimentSearchFilter"] + parameters: InvalidExperimentSearchFilterParameters + error_instance_id: str + + class InvalidModelApiParameters(typing_extensions.TypedDict): """The model api failed validations""" @@ -511,6 +530,7 @@ class TransformJsonLiveDeploymentPermissionDenied(errors.PermissionDeniedError): "InferenceFailure", "InferenceInvalidInput", "InferenceTimeout", + "InvalidExperimentSearchFilter", "InvalidModelApi", "InvalidModelStudioCreateRequest", "JsonExperimentArtifactTablePermissionDenied", diff --git a/foundry_sdk/v2/models/experiment.py b/foundry_sdk/v2/models/experiment.py index 924ce3f56..31be2e979 100644 --- a/foundry_sdk/v2/models/experiment.py +++ b/foundry_sdk/v2/models/experiment.py @@ -161,6 +161,7 @@ def search( :return: Returns the result object. :rtype: models_models.SearchExperimentsResponse + :raises InvalidExperimentSearchFilter: The search filter is invalid. This can occur when using an unsupported operator and value type combination in a parameter filter, filtering by an unsupported status, or providing a malformed filter. :raises SearchExperimentsPermissionDenied: Could not search the Experiment. """ @@ -187,6 +188,7 @@ def search( response_type=models_models.SearchExperimentsResponse, request_timeout=request_timeout, throwable_errors={ + "InvalidExperimentSearchFilter": models_errors.InvalidExperimentSearchFilter, "SearchExperimentsPermissionDenied": models_errors.SearchExperimentsPermissionDenied, }, response_mode=_sdk_internal.get("response_mode"), @@ -347,6 +349,7 @@ def search( :return: Returns the result object. :rtype: typing.Awaitable[models_models.SearchExperimentsResponse] + :raises InvalidExperimentSearchFilter: The search filter is invalid. This can occur when using an unsupported operator and value type combination in a parameter filter, filtering by an unsupported status, or providing a malformed filter. :raises SearchExperimentsPermissionDenied: Could not search the Experiment. """ @@ -373,6 +376,7 @@ def search( response_type=models_models.SearchExperimentsResponse, request_timeout=request_timeout, throwable_errors={ + "InvalidExperimentSearchFilter": models_errors.InvalidExperimentSearchFilter, "SearchExperimentsPermissionDenied": models_errors.SearchExperimentsPermissionDenied, }, response_mode=_sdk_internal.get("response_mode"), diff --git a/foundry_sdk/v2/models/models.py b/foundry_sdk/v2/models/models.py index 5d444291e..dbbfe5e50 100644 --- a/foundry_sdk/v2/models/models.py +++ b/foundry_sdk/v2/models/models.py @@ -123,14 +123,14 @@ class DoubleParameter(core.ModelBase): class DoubleSeriesAggregations(core.ModelBase): """Aggregated statistics for numeric series.""" - min: float - """Minimum value in the series""" + min: typing.Optional[float] = None + """Minimum value in the series. Absent if the metric has not been computed.""" - max: float - """Maximum value in the series""" + max: typing.Optional[float] = None + """Maximum value in the series. Absent if the metric has not been computed.""" - last: float - """Most recent value in the series""" + last: typing.Optional[float] = None + """Most recent value in the series. Absent if the metric has not been computed.""" type: typing.Literal["double"] = "double" @@ -164,8 +164,13 @@ class Experiment(core.ModelBase): rid: ExperimentRid model_rid: ModelRid = pydantic.Field(alias=str("modelRid")) # type: ignore[literal-required] - name: ExperimentName - created_at: core_models.CreatedTime = pydantic.Field(alias=str("createdAt")) # type: ignore[literal-required] + name: typing.Optional[str] = None + """ + The display name of the experiment. Present in search results but not available when + retrieving a single experiment via the get endpoint. + """ + + created_time: core_models.CreatedTime = pydantic.Field(alias=str("createdTime")) # type: ignore[literal-required] created_by: core_models.CreatedBy = pydantic.Field(alias=str("createdBy")) # type: ignore[literal-required] source: ExperimentSource status: ExperimentStatus @@ -212,10 +217,6 @@ class ExperimentCodeWorkspaceSource(core.ModelBase): type: typing.Literal["codeWorkspace"] = "codeWorkspace" -ExperimentName = str -"""ExperimentName""" - - ExperimentRid = core.RID """The Resource Identifier (RID) of an Experiment.""" @@ -751,10 +752,6 @@ class SearchExperimentsEqualsFilter(core.ModelBase): """ -SearchExperimentsFilterOperator = typing.Literal["EQ", "GT", "LT", "CONTAINS"] -"""Comparison operator for compound filter predicates.""" - - class SearchExperimentsNotFilter(core.ModelBase): """Returns experiments where the filter is not satisfied.""" @@ -762,6 +759,10 @@ class SearchExperimentsNotFilter(core.ModelBase): type: typing.Literal["not"] = "not" +SearchExperimentsNumericFilterOperator = typing.Literal["EQ", "GT", "LT"] +"""Comparison operator for numeric filter predicates (series and summary metrics).""" + + class SearchExperimentsOrFilter(core.ModelBase): """Returns experiments where at least one filter is satisfied.""" @@ -776,7 +777,7 @@ class SearchExperimentsOrderBy(core.ModelBase): direction: core_models.OrderByDirection -SearchExperimentsOrderByField = typing.Literal["EXPERIMENT_NAME", "CREATED_AT"] +SearchExperimentsOrderByField = typing.Literal["EXPERIMENT_NAME", "CREATED_TIME"] """Fields to order experiment search results by.""" @@ -785,15 +786,15 @@ class SearchExperimentsParameterFilter(core.ModelBase): Filter that atomically binds a parameter name to a value comparison, ensuring both conditions are evaluated on the same parameter. Supported combinations: - - EQ: boolean, double, integer, datetime, or string value + - EQ: boolean, double, integer, or datetime value - GT/LT: double, integer, or datetime value - - CONTAINS: string value (matches the parameter's string value) + - CONTAINS: string value (substring match on the parameter's string value) """ parameter_name: ParameterName = pydantic.Field(alias=str("parameterName")) # type: ignore[literal-required] """The exact name of the parameter to filter on.""" - operator: SearchExperimentsFilterOperator + operator: SearchExperimentsParameterFilterOperator """The comparison operator to apply.""" value: typing.Any @@ -802,6 +803,10 @@ class SearchExperimentsParameterFilter(core.ModelBase): type: typing.Literal["parameterFilter"] = "parameterFilter" +SearchExperimentsParameterFilterOperator = typing.Literal["EQ", "GT", "LT", "CONTAINS"] +"""Comparison operator for parameter filter predicates.""" + + class SearchExperimentsRequest(core.ModelBase): """SearchExperimentsRequest""" @@ -840,7 +845,7 @@ class SearchExperimentsSeriesFilter(core.ModelBase): field: SearchExperimentsSeriesFilterField """The series metric to compare.""" - operator: SearchExperimentsFilterOperator + operator: SearchExperimentsNumericFilterOperator """The comparison operator (EQ, GT, or LT).""" value: typing.Any @@ -881,7 +886,7 @@ class SearchExperimentsSummaryMetricFilter(core.ModelBase): aggregation: SummaryMetricAggregation """The aggregation type (MIN, MAX, LAST).""" - operator: SearchExperimentsFilterOperator + operator: SearchExperimentsNumericFilterOperator """The comparison operator (EQ, GT, or LT).""" value: typing.Any @@ -896,8 +901,8 @@ class SeriesAggregations(core.ModelBase): name: SeriesName """The series name""" - length: core.Long - """Number of values in the series""" + length: typing.Optional[core.Long] = None + """Number of values in the series. This field may be absent when series aggregations are derived from summary metrics rather than the full series data.""" value: SeriesAggregationsValue """Aggregated values for this series""" @@ -1082,7 +1087,6 @@ class UnsupportedTypeError(core.ModelBase): "ExperimentAuthoringSource", "ExperimentBranch", "ExperimentCodeWorkspaceSource", - "ExperimentName", "ExperimentRid", "ExperimentSdkSource", "ExperimentSource", @@ -1146,12 +1150,13 @@ class UnsupportedTypeError(core.ModelBase): "SearchExperimentsEqualsFilter", "SearchExperimentsEqualsFilterField", "SearchExperimentsFilter", - "SearchExperimentsFilterOperator", "SearchExperimentsNotFilter", + "SearchExperimentsNumericFilterOperator", "SearchExperimentsOrFilter", "SearchExperimentsOrderBy", "SearchExperimentsOrderByField", "SearchExperimentsParameterFilter", + "SearchExperimentsParameterFilterOperator", "SearchExperimentsRequest", "SearchExperimentsResponse", "SearchExperimentsSeriesFilter", diff --git a/foundry_sdk/v2/ontologies/action_type.py b/foundry_sdk/v2/ontologies/action_type.py index 059fbc553..2d215c145 100644 --- a/foundry_sdk/v2/ontologies/action_type.py +++ b/foundry_sdk/v2/ontologies/action_type.py @@ -15,6 +15,7 @@ import typing +import annotated_types import pydantic import typing_extensions @@ -145,6 +146,65 @@ def get_by_rid( ), ) + @core.maybe_ignore_preview + @pydantic.validate_call + @errors.handle_unexpected + def get_by_rid_batch( + self, + ontology: ontologies_models.OntologyIdentifier, + *, + requests: typing_extensions.Annotated[ + typing.List[ontologies_models.GetActionTypeByRidBatchRequestElement], + annotated_types.Len(min_length=1, max_length=100), + ], + branch: typing.Optional[core_models.FoundryBranch] = None, + request_timeout: typing.Optional[core.Timeout] = None, + _sdk_internal: core.SdkInternal = {}, + ) -> ontologies_models.GetActionTypeByRidBatchResponse: + """ + Gets a list of action types by RID in bulk. + + Action types are filtered from the response if they don't exist or the requesting token lacks the required + permissions. + + The maximum batch size for this endpoint is 100. + + :param ontology: + :type ontology: OntologyIdentifier + :param requests: + :type requests: List[GetActionTypeByRidBatchRequestElement] + :param branch: The Foundry branch to load the action type definitions from. If not specified, the default branch will be used. Branches are an experimental feature and not all workflows are supported. + :type branch: Optional[FoundryBranch] + :param request_timeout: timeout setting for this request in seconds. + :type request_timeout: Optional[int] + :return: Returns the result object. + :rtype: ontologies_models.GetActionTypeByRidBatchResponse + """ + + return self._api_client.call_api( + core.RequestInfo( + method="POST", + resource_path="/v2/ontologies/{ontology}/actionTypes/getByRidBatch", + query_params={ + "branch": branch, + }, + path_params={ + "ontology": ontology, + }, + header_params={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + body=ontologies_models.GetActionTypeByRidBatchRequest( + requests=requests, + ), + response_type=ontologies_models.GetActionTypeByRidBatchResponse, + request_timeout=request_timeout, + throwable_errors={}, + response_mode=_sdk_internal.get("response_mode"), + ), + ) + @core.maybe_ignore_preview @pydantic.validate_call @errors.handle_unexpected @@ -206,10 +266,12 @@ class _ActionTypeClientRaw: def __init__(self, client: ActionTypeClient) -> None: def get(_: ontologies_models.ActionTypeV2): ... def get_by_rid(_: ontologies_models.ActionTypeV2): ... + def get_by_rid_batch(_: ontologies_models.GetActionTypeByRidBatchResponse): ... def list(_: ontologies_models.ListActionTypesResponseV2): ... self.get = core.with_raw_response(get, client.get) self.get_by_rid = core.with_raw_response(get_by_rid, client.get_by_rid) + self.get_by_rid_batch = core.with_raw_response(get_by_rid_batch, client.get_by_rid_batch) self.list = core.with_raw_response(list, client.list) @@ -217,10 +279,14 @@ class _ActionTypeClientStreaming: def __init__(self, client: ActionTypeClient) -> None: def get(_: ontologies_models.ActionTypeV2): ... def get_by_rid(_: ontologies_models.ActionTypeV2): ... + def get_by_rid_batch(_: ontologies_models.GetActionTypeByRidBatchResponse): ... def list(_: ontologies_models.ListActionTypesResponseV2): ... self.get = core.with_streaming_response(get, client.get) self.get_by_rid = core.with_streaming_response(get_by_rid, client.get_by_rid) + self.get_by_rid_batch = core.with_streaming_response( + get_by_rid_batch, client.get_by_rid_batch + ) self.list = core.with_streaming_response(list, client.list) @@ -345,6 +411,65 @@ def get_by_rid( ), ) + @core.maybe_ignore_preview + @pydantic.validate_call + @errors.handle_unexpected + def get_by_rid_batch( + self, + ontology: ontologies_models.OntologyIdentifier, + *, + requests: typing_extensions.Annotated[ + typing.List[ontologies_models.GetActionTypeByRidBatchRequestElement], + annotated_types.Len(min_length=1, max_length=100), + ], + branch: typing.Optional[core_models.FoundryBranch] = None, + request_timeout: typing.Optional[core.Timeout] = None, + _sdk_internal: core.SdkInternal = {}, + ) -> typing.Awaitable[ontologies_models.GetActionTypeByRidBatchResponse]: + """ + Gets a list of action types by RID in bulk. + + Action types are filtered from the response if they don't exist or the requesting token lacks the required + permissions. + + The maximum batch size for this endpoint is 100. + + :param ontology: + :type ontology: OntologyIdentifier + :param requests: + :type requests: List[GetActionTypeByRidBatchRequestElement] + :param branch: The Foundry branch to load the action type definitions from. If not specified, the default branch will be used. Branches are an experimental feature and not all workflows are supported. + :type branch: Optional[FoundryBranch] + :param request_timeout: timeout setting for this request in seconds. + :type request_timeout: Optional[int] + :return: Returns the result object. + :rtype: typing.Awaitable[ontologies_models.GetActionTypeByRidBatchResponse] + """ + + return self._api_client.call_api( + core.RequestInfo( + method="POST", + resource_path="/v2/ontologies/{ontology}/actionTypes/getByRidBatch", + query_params={ + "branch": branch, + }, + path_params={ + "ontology": ontology, + }, + header_params={ + "Content-Type": "application/json", + "Accept": "application/json", + }, + body=ontologies_models.GetActionTypeByRidBatchRequest( + requests=requests, + ), + response_type=ontologies_models.GetActionTypeByRidBatchResponse, + request_timeout=request_timeout, + throwable_errors={}, + response_mode=_sdk_internal.get("response_mode"), + ), + ) + @core.maybe_ignore_preview @pydantic.validate_call @errors.handle_unexpected @@ -406,10 +531,14 @@ class _AsyncActionTypeClientRaw: def __init__(self, client: AsyncActionTypeClient) -> None: def get(_: ontologies_models.ActionTypeV2): ... def get_by_rid(_: ontologies_models.ActionTypeV2): ... + def get_by_rid_batch(_: ontologies_models.GetActionTypeByRidBatchResponse): ... def list(_: ontologies_models.ListActionTypesResponseV2): ... self.get = core.async_with_raw_response(get, client.get) self.get_by_rid = core.async_with_raw_response(get_by_rid, client.get_by_rid) + self.get_by_rid_batch = core.async_with_raw_response( + get_by_rid_batch, client.get_by_rid_batch + ) self.list = core.async_with_raw_response(list, client.list) @@ -417,8 +546,12 @@ class _AsyncActionTypeClientStreaming: def __init__(self, client: AsyncActionTypeClient) -> None: def get(_: ontologies_models.ActionTypeV2): ... def get_by_rid(_: ontologies_models.ActionTypeV2): ... + def get_by_rid_batch(_: ontologies_models.GetActionTypeByRidBatchResponse): ... def list(_: ontologies_models.ListActionTypesResponseV2): ... self.get = core.async_with_streaming_response(get, client.get) self.get_by_rid = core.async_with_streaming_response(get_by_rid, client.get_by_rid) + self.get_by_rid_batch = core.async_with_streaming_response( + get_by_rid_batch, client.get_by_rid_batch + ) self.list = core.async_with_streaming_response(list, client.list) diff --git a/foundry_sdk/v2/ontologies/models.py b/foundry_sdk/v2/ontologies/models.py index d841f2541..c4ee5411a 100644 --- a/foundry_sdk/v2/ontologies/models.py +++ b/foundry_sdk/v2/ontologies/models.py @@ -18,6 +18,7 @@ import typing from datetime import date +import annotated_types import pydantic import typing_extensions @@ -1414,6 +1415,27 @@ class GeotimeSeriesValue(core.ModelBase): type: typing.Literal["geotimeSeriesValue"] = "geotimeSeriesValue" +class GetActionTypeByRidBatchRequest(core.ModelBase): + """GetActionTypeByRidBatchRequest""" + + requests: typing_extensions.Annotated[ + typing.List[GetActionTypeByRidBatchRequestElement], + annotated_types.Len(min_length=1, max_length=100), + ] + + +class GetActionTypeByRidBatchRequestElement(core.ModelBase): + """GetActionTypeByRidBatchRequestElement""" + + action_type_rid: ActionTypeRid = pydantic.Field(alias=str("actionTypeRid")) # type: ignore[literal-required] + + +class GetActionTypeByRidBatchResponse(core.ModelBase): + """GetActionTypeByRidBatchResponse""" + + data: typing.List[ActionTypeV2] + + class GetSelectedPropertyOperation(core.ModelBase): """ Gets a single value of a property. Throws if the target object set is on the MANY side of the link and could @@ -3807,6 +3829,7 @@ class QueryArrayType(core.ModelBase): "OntologyInterfaceObjectType", "QueryStructType", "QuerySetType", + core_models.VoidType, core_models.StringType, "EntrySetType", core_models.DoubleType, @@ -3965,7 +3988,26 @@ class RegexConstraint(core.ModelBase): class RegexQuery(core.ModelBase): """ Returns objects where the specified field matches the regex pattern provided. This applies to the non-analyzed - form of text fields and supports standard regex syntax of dot (.), star(*) and question mark(?). + form of text fields. Supported operators: + - `.` matches any character. + - `?` repeats the previous character 0 or 1 times. + - `+` repeats the previous character 1 or more times. + - `*` repeats the previous character 0 or more times. + - `{}` defines the minimum and maximum number of times the preceding character can repeat. `{2}` means the + previous character must repeat only twice, `{2,}` means the previous character must repeat at least twice, + and `{2,4}` means the previous character must repeat between 2-4 times. + - `|` is the OR operator. + - `()` forms a group within an expression such that the group can be treated as a single character. + - `[]` matches a single one of the characters contained inside the brackets, meaning [abc] matches `a`, `b` or + `c`. Unless `-` is the first character or escaped with `\\` (in which case it is treated as a normal character), + `-` can be used inside the bracket to create a range of characters, meaning [a-c] matches `a`, `b`, or `c`. + If the character sequence inside the brackets begins with `^`, the set of characters is negated, meaning + [^abc] does not match `a`, `b`, or `c`. Otherwise, `^` is treated as a normal character. + - `"` creates groups of string literals. + - `\\` is used as an escape character. However, \\d and \\D match digit and non-digit characters respectively, \\s + and \\S match whitespace and non whitespace characters respectively, and \\w and \\W match word and non word + characters respectively. + Either `field` or `propertyIdentifier` can be supplied, but not both. """ @@ -5466,6 +5508,9 @@ class WithinPolygonQuery(core.ModelBase): "GeoShapeV2Query", "GeotemporalSeriesEntry", "GeotimeSeriesValue", + "GetActionTypeByRidBatchRequest", + "GetActionTypeByRidBatchRequestElement", + "GetActionTypeByRidBatchResponse", "GetSelectedPropertyOperation", "GreatestPropertyExpression", "GroupMemberConstraint", diff --git a/foundry_sdk/v2/ontologies/query.py b/foundry_sdk/v2/ontologies/query.py index 706b21fce..70fb7a1fd 100644 --- a/foundry_sdk/v2/ontologies/query.py +++ b/foundry_sdk/v2/ontologies/query.py @@ -59,6 +59,7 @@ def execute( ontologies_models.ParameterId, typing.Optional[ontologies_models.DataValue] ], attribution: typing.Optional[core_models.Attribution] = None, + branch: typing.Optional[core_models.FoundryBranch] = None, sdk_package_rid: typing.Optional[ontologies_models.SdkPackageRid] = None, sdk_version: typing.Optional[ontologies_models.SdkVersion] = None, trace_parent: typing.Optional[core_models.TraceParent] = None, @@ -81,6 +82,8 @@ def execute( :type parameters: Dict[ParameterId, Optional[DataValue]] :param attribution: The Attribution to be used when executing this request. :type attribution: Optional[Attribution] + :param branch: The Foundry branch to execute the query from. If not specified, the default branch is used. Branches are an experimental feature and not all workflows are supported. When provided without `version`, the latest version on this branch is used, including pre-release versions. When provided with `version`, the specified version must exist on the branch. + :type branch: Optional[FoundryBranch] :param sdk_package_rid: The package rid of the generated SDK. :type sdk_package_rid: Optional[SdkPackageRid] :param sdk_version: The version of the generated SDK. @@ -91,7 +94,7 @@ def execute( :type trace_state: Optional[TraceState] :param transaction_id: The ID of an Ontology transaction to read from. Transactions are an experimental feature and all workflows may not be supported. :type transaction_id: Optional[OntologyTransactionId] - :param version: The version of the Query to execute. + :param version: The version of the Query to execute. When used with `branch`, the specified version must exist on the branch. :type version: Optional[FunctionVersion] :param request_timeout: timeout setting for this request in seconds. :type request_timeout: Optional[int] @@ -104,6 +107,7 @@ def execute( method="POST", resource_path="/v2/ontologies/{ontology}/queries/{queryApiName}/execute", query_params={ + "branch": branch, "sdkPackageRid": sdk_package_rid, "sdkVersion": sdk_version, "transactionId": transaction_id, @@ -180,6 +184,7 @@ def execute( ontologies_models.ParameterId, typing.Optional[ontologies_models.DataValue] ], attribution: typing.Optional[core_models.Attribution] = None, + branch: typing.Optional[core_models.FoundryBranch] = None, sdk_package_rid: typing.Optional[ontologies_models.SdkPackageRid] = None, sdk_version: typing.Optional[ontologies_models.SdkVersion] = None, trace_parent: typing.Optional[core_models.TraceParent] = None, @@ -202,6 +207,8 @@ def execute( :type parameters: Dict[ParameterId, Optional[DataValue]] :param attribution: The Attribution to be used when executing this request. :type attribution: Optional[Attribution] + :param branch: The Foundry branch to execute the query from. If not specified, the default branch is used. Branches are an experimental feature and not all workflows are supported. When provided without `version`, the latest version on this branch is used, including pre-release versions. When provided with `version`, the specified version must exist on the branch. + :type branch: Optional[FoundryBranch] :param sdk_package_rid: The package rid of the generated SDK. :type sdk_package_rid: Optional[SdkPackageRid] :param sdk_version: The version of the generated SDK. @@ -212,7 +219,7 @@ def execute( :type trace_state: Optional[TraceState] :param transaction_id: The ID of an Ontology transaction to read from. Transactions are an experimental feature and all workflows may not be supported. :type transaction_id: Optional[OntologyTransactionId] - :param version: The version of the Query to execute. + :param version: The version of the Query to execute. When used with `branch`, the specified version must exist on the branch. :type version: Optional[FunctionVersion] :param request_timeout: timeout setting for this request in seconds. :type request_timeout: Optional[int] @@ -225,6 +232,7 @@ def execute( method="POST", resource_path="/v2/ontologies/{ontology}/queries/{queryApiName}/execute", query_params={ + "branch": branch, "sdkPackageRid": sdk_package_rid, "sdkVersion": sdk_version, "transactionId": transaction_id, diff --git a/foundry_sdk/v2/sql_queries/models.py b/foundry_sdk/v2/sql_queries/models.py index 0a7136439..2ff3b34a4 100644 --- a/foundry_sdk/v2/sql_queries/models.py +++ b/foundry_sdk/v2/sql_queries/models.py @@ -151,13 +151,6 @@ class ParameterAnyValue(core.ModelBase): type: typing.Literal["any"] = "any" -class ParameterBinaryValue(core.ModelBase): - """A binary parameter value.""" - - value: bytes - type: typing.Literal["binary"] = "binary" - - class ParameterBooleanValue(core.ModelBase): """A boolean parameter value.""" @@ -277,7 +270,6 @@ class ParameterTimestampValue(core.ModelBase): "ParameterLongValue", "ParameterBooleanValue", "ParameterNullValue", - "ParameterBinaryValue", "ParameterShortValue", "ParameterDecimalValue", "ParameterMapValue", @@ -393,7 +385,6 @@ class UnnamedParameterValues(core.ModelBase): "MapParameterKey", "NamedParameterMapping", "ParameterAnyValue", - "ParameterBinaryValue", "ParameterBooleanValue", "ParameterDateValue", "ParameterDecimalValue", diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/auth/__init__.py b/tests/auth/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/auth/test_confidential_client.py b/tests/auth/test_confidential_client.py deleted file mode 100644 index 92b8ac8f7..000000000 --- a/tests/auth/test_confidential_client.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import contextlib - -import httpx -import pytest -from mockito import any -from mockito import unstub -from mockito import verify -from mockito import when - -from foundry_sdk._core.confidential_client_auth import ConfidentialClientAuth - -RESPONSE = { - "access_token": "access_token", - "token_type": "foo", - "expires_in": 3600, -} - - -@contextlib.contextmanager -def stubbed_auth(should_refresh=True, token_response=RESPONSE): - auth = ConfidentialClientAuth( - client_id="client_id", - client_secret="client_secret", - hostname="https://a.b.c.com", - should_refresh=should_refresh, - ) - - response = httpx.Response( - request=httpx.Request("GET", "foo"), status_code=200, json=token_response - ) - when(auth._get_client()).post("/multipass/api/oauth2/token", data=any()).thenReturn(response) - - response = httpx.Response(request=httpx.Request("GET", "foo"), status_code=200) - when(auth._get_client()).post("/multipass/api/oauth2/revoke_token", data=any()).thenReturn( - response - ) - - when(auth)._try_refresh_token().thenCallOriginalImplementation() - when(auth).sign_out().thenCallOriginalImplementation() - - yield auth - unstub() - - -def test_confidential_client_instantiate(): - auth = ConfidentialClientAuth( - client_id="client_id", - client_secret="client_secret", - hostname="https://a.b.c.com", - should_refresh=True, - ) - assert auth._client_id == "client_id" - assert auth._client_secret == "client_secret" - assert auth._hostname == "https://a.b.c.com" - assert auth._token is None - assert auth.url == "a.b.c.com" - assert auth._should_refresh - - -def test_confidential_client_url(): - assert ( - ConfidentialClientAuth(client_id="1", client_secret="1", hostname="https://a.b.c.com").url - == "a.b.c.com" - ) - assert ( - ConfidentialClientAuth(client_id="1", client_secret="1", hostname="http://a.b.c.com").url - == "a.b.c.com" - ) - assert ( - ConfidentialClientAuth(client_id="1", client_secret="1", hostname="a.b.c.com/").url - == "a.b.c.com" - ) - - -def test_confidential_client_get_token(): - with stubbed_auth() as auth: - assert auth.get_token().access_token == "access_token" - - -def test_confidential_client_sign_out(): - with stubbed_auth() as auth: - auth.get_token() - assert auth._token is not None - auth.sign_out() - assert auth._token is None - assert auth._stop_refresh_event._flag - - -def test_confidential_client_execute_with_token_successful_method(): - with stubbed_auth() as auth: - assert auth.execute_with_token(lambda _: httpx.Response(200)).status_code == 200 - verify(auth, times=0)._refresh_token() - - -def test_confidential_client_execute_with_token_failing_method(): - with stubbed_auth() as auth: - - def raise_(ex): - raise ex - - with pytest.raises(ValueError): - auth.execute_with_token(lambda _: raise_(ValueError("Oops!"))) - - verify(auth, times=0)._refresh_token() - verify(auth, times=0).sign_out() - - -def test_confidential_client_execute_with_token_method_raises_401(): - with stubbed_auth() as auth: - - def raise_401(): - e = httpx.HTTPStatusError( - "foo", - request=httpx.Request("foo", url="foo"), - response=httpx.Response(status_code=401), - ) - raise e - - with pytest.raises(httpx.HTTPStatusError): - auth.execute_with_token(lambda _: raise_401()) - - verify(auth, times=1)._try_refresh_token() - verify(auth, times=1).sign_out() - - -def test_invalid_client_id_raises_appropriate_error(): - assert pytest.raises(TypeError, lambda: ConfidentialClientAuth()) # type: ignore - assert pytest.raises(TypeError, lambda: ConfidentialClientAuth(1, "1")) # type: ignore - assert pytest.raises(TypeError, lambda: ConfidentialClientAuth(None, "1")) # type: ignore - assert pytest.raises(ValueError, lambda: ConfidentialClientAuth("", "1")) - - -def test_invalid_client_secret_raises_appropriate_error(): - assert pytest.raises(TypeError, lambda: ConfidentialClientAuth("1")) # type: ignore - assert pytest.raises(TypeError, lambda: ConfidentialClientAuth("1", 1)) # type: ignore - assert pytest.raises(TypeError, lambda: ConfidentialClientAuth("1", None)) # type: ignore - assert pytest.raises(ValueError, lambda: ConfidentialClientAuth("1", "")) - - -def test_invalid_hostname_raises_appropriate_error(): - assert pytest.raises(TypeError, lambda: ConfidentialClientAuth("1", "1", 1)) # type: ignore - assert pytest.raises(ValueError, lambda: ConfidentialClientAuth("1", "1", "")) # type: ignore - - -def test_invalid_scopes_raises_appropriate_error(): - assert pytest.raises(TypeError, lambda: ConfidentialClientAuth("1", "1", scopes=1)) # type: ignore diff --git a/tests/auth/test_confidential_client_oauth_flow_provider.py b/tests/auth/test_confidential_client_oauth_flow_provider.py deleted file mode 100644 index fd9d6bdaf..000000000 --- a/tests/auth/test_confidential_client_oauth_flow_provider.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import httpx -import pytest -from expects import equal -from expects import expect -from mockito import mock -from mockito import unstub -from mockito import when - -from foundry_sdk._core.http_client import HttpClient -from foundry_sdk._core.oauth_utils import ConfidentialClientOAuthFlowProvider - - -@pytest.fixture(name="client", scope="module") -def instantiate_server_oauth_flow_provider(): - return ConfidentialClientOAuthFlowProvider( - client_id="client_id", - client_secret="client_secret", - multipass_context_path="/multipass", - scopes=["scope1", "scope2"], - ) - - -@pytest.fixture(scope="module") -def http_client(): - return HttpClient("https://a.b.c.com") - - -def test_get_token(client, http_client): - response = mock(httpx.Response) - when(response).raise_for_status().thenReturn(None) - when(response).json().thenReturn( - {"access_token": "example_token", "expires_in": 42, "token_type": "Bearer"} - ) - when(http_client).post( - "/multipass/api/oauth2/token", - data={ - "client_id": "client_id", - "client_secret": "client_secret", - "grant_type": "client_credentials", - "scope": "scope1 scope2 offline_access", - }, - ).thenReturn(response) - token = client.get_token(http_client) - expect(token.access_token).to(equal("example_token")) - expect(token.token_type).to(equal("Bearer")) - unstub() - - -def test_get_token_throws_when_unsuccessful(client, http_client): - response = mock(httpx.Response) - when(response).raise_for_status().thenRaise( - httpx.HTTPStatusError( - "Foo", - request=httpx.Request("GET", "/foo/bar"), - response=httpx.Response(200), - ), - ) - when(http_client).post( - "/multipass/api/oauth2/token", - data={ - "client_id": "client_id", - "client_secret": "client_secret", - "grant_type": "client_credentials", - "scope": "scope1 scope2 offline_access", - }, - ).thenReturn(response) - - with pytest.raises(httpx.HTTPStatusError): - client.get_token(http_client) - - unstub() - - -def test_revoke_token(client, http_client): - response = mock(httpx.Response) - when(response).raise_for_status().thenReturn(None) - when(http_client).post( - "/multipass/api/oauth2/revoke_token", - data={ - "client_id": "client_id", - "client_secret": "client_secret", - "token": "token_to_be_revoked", - }, - ).thenReturn(response) - client.revoke_token(http_client, "token_to_be_revoked") - unstub() - - -def test_get_scopes(client): - expect(client.get_scopes()).to(equal(["scope1", "scope2", "offline_access"])) diff --git a/tests/auth/test_oauth_utils.py b/tests/auth/test_oauth_utils.py deleted file mode 100644 index 707ab5998..000000000 --- a/tests/auth/test_oauth_utils.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from expects import equal -from expects import expect -from mockito import unstub -from mockito import when - -from foundry_sdk._core.oauth_utils import ConfidentialClientOAuthFlowProvider -from foundry_sdk._core.oauth_utils import OAuthToken -from foundry_sdk._core.oauth_utils import OAuthTokenResponse -from foundry_sdk._core.oauth_utils import OAuthUtils -from foundry_sdk._core.oauth_utils import PublicClientOAuthFlowProvider - - -def test_get_token_uri(): - expect(OAuthUtils.get_token_uri()).to(equal("/multipass/api/oauth2/token")) - - -def test_get_authorize_uri(): - expect(OAuthUtils.get_authorize_uri()).to(equal("/multipass/api/oauth2/authorize")) - - -def test_get_revoke_uri(): - expect(OAuthUtils.get_revoke_uri()).to(equal("/multipass/api/oauth2/revoke_token")) - - -def test_create_uri(): - expect(OAuthUtils.create_uri("/api/v2/datasets", "/abc")).to(equal("/api/v2/datasets/abc")) - expect(OAuthUtils.create_uri("/api/v2/datasets", "/abc")).to(equal("/api/v2/datasets/abc")) - - -def test_confidential_client_no_scopes(): - provider = ConfidentialClientOAuthFlowProvider("CLIENT_ID", "CLIENT_SECRET", "URL", scopes=None) - assert provider.get_scopes() == [] - - provider.scopes = [] - assert provider.get_scopes() == [] - - -def test_confidential_client_with_scopes(): - provider = ConfidentialClientOAuthFlowProvider( - "CLIENT_ID", "CLIENT_SECRET", "URL", scopes=["test"] - ) - assert provider.get_scopes() == ["test", "offline_access"] - - -def test_public_client_no_scopes(): - provider = PublicClientOAuthFlowProvider("CLIENT_ID", "REDIRECT_URL", "URL", scopes=None) - assert provider.get_scopes() == [] - - provider.scopes = [] - assert provider.get_scopes() == [] - - -def test_public_client_with_scopes(): - provider = PublicClientOAuthFlowProvider("CLIENT_ID", "REDIRECT_URL", "URL", scopes=["test"]) - assert provider.get_scopes() == ["test", "offline_access"] - - -def test_token_from_dict(): - import foundry_sdk._core.oauth_utils as module_under_test - - when(module_under_test.time).time().thenReturn(123) - token = OAuthToken( - OAuthTokenResponse( - {"access_token": "example_token", "expires_in": 42, "token_type": "Bearer"} - ) - ) - expect(token.access_token).to(equal("example_token")) - expect(token.token_type).to(equal("Bearer")) - expect(token.expires_in).to(equal(42)) - expect(token.expires_at).to(equal(123 * 1000 + 42 * 1000)) - expect(token._calculate_expiration()).to(equal(123 * 1000 + 42 * 1000)) - unstub() diff --git a/tests/auth/test_public_client.py b/tests/auth/test_public_client.py deleted file mode 100644 index a5c7850b0..000000000 --- a/tests/auth/test_public_client.py +++ /dev/null @@ -1,174 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import contextlib - -import httpx -import pytest -from mockito import any -from mockito import unstub -from mockito import verify -from mockito import when - -from foundry_sdk._core.auth_utils import Token -from foundry_sdk._core.public_client_auth import PublicClientAuth -from foundry_sdk._errors.not_authenticated import NotAuthenticated - -RESPONSE = { - "access_token": "access_token", - "token_type": "foo", - "expires_in": 3600, - "refresh_token": "bar", -} - - -@contextlib.contextmanager -def stubbed_auth(should_refresh=True, token_response=RESPONSE): - auth = PublicClientAuth( - client_id="client_id", - redirect_url="redirect_url", - hostname="https://a.b.c.com", - should_refresh=should_refresh, - ) - - response = httpx.Response( - request=httpx.Request("GET", "foo"), status_code=200, json=token_response - ) - when(auth._get_client()).post( - "/multipass/api/oauth2/token", - headers={"Content-Type": "application/x-www-form-urlencoded"}, - data=any(), - ).thenReturn(response) - - response = httpx.Response(request=httpx.Request("GET", "foo"), status_code=200) - when(auth._get_client()).post("/multipass/api/oauth2/revoke_token", data=any()).thenReturn( - response - ) - - when(auth)._try_refresh_token().thenCallOriginalImplementation() - when(auth).sign_out().thenCallOriginalImplementation() - - yield auth - unstub() - - -def _sign_in(auth: PublicClientAuth): - auth.sign_in() - assert auth._auth_request is not None - auth.set_token(code="", state=auth._auth_request.state) - - -def test_public_client_instantiate(): - auth = PublicClientAuth( - client_id="client_id", - redirect_url="redirect_url", - hostname="https://a.b.c.com", - should_refresh=True, - ) - assert auth._client_id == "client_id" - assert auth._redirect_url == "redirect_url" - assert auth._token is None - assert auth.url == "a.b.c.com" - assert auth._should_refresh - - -def test_public_client_sign_in(): - with stubbed_auth() as auth: - assert auth.sign_in().startswith("https://a.b.c.com/multipass/api/oauth2/authorize?") - assert auth._auth_request is not None - - -def test_public_client_set_token(): - with stubbed_auth() as auth: - auth.sign_in() - assert auth._auth_request is not None - - auth.set_token(code="", state=auth._auth_request.state) - assert auth._token is not None - assert auth._token.access_token == "access_token" - - -def test_public_client_url(): - assert ( - PublicClientAuth(client_id="", redirect_url="", hostname="https://a.b.c.com").url - == "a.b.c.com" - ) - assert ( - PublicClientAuth(client_id="", redirect_url="", hostname="http://a.b.c.com").url - == "a.b.c.com" - ) - assert PublicClientAuth(client_id="", redirect_url="", hostname="a.b.c.com/").url == "a.b.c.com" - - -def test_public_client_get_token(): - with stubbed_auth() as auth: - _sign_in(auth) - assert isinstance(auth.get_token(), Token) - - -def test_public_client_sign_out(): - with stubbed_auth() as auth: - _sign_in(auth) - assert auth._token is not None - - auth.sign_out() - assert auth._token is None - assert auth._stop_refresh_event._flag - - -def test_public_client_get_token_throws_if_not_signed_in(): - with stubbed_auth() as auth: - with pytest.raises(NotAuthenticated) as e: - auth.get_token() - - assert str(e.value) == "Client has not been authenticated." - - -def test_public_client_execute_with_token_successful_method(): - with stubbed_auth() as auth: - _sign_in(auth) - assert auth.execute_with_token(lambda _: httpx.Response(200)).status_code == 200 - verify(auth, times=0)._refresh_token() - - -def test_public_client_execute_with_token_failing_method(): - with stubbed_auth() as auth: - _sign_in(auth) - - def raise_(ex): - raise ex - - with pytest.raises(ValueError): - auth.execute_with_token(lambda _: raise_(ValueError("Oops!"))) - - verify(auth, times=0)._refresh_token() - - -def test_public_client_execute_with_token_method_raises_401(): - with stubbed_auth() as auth: - _sign_in(auth) - - def raise_401(): - e = httpx.HTTPStatusError( - "foo", - request=httpx.Request("foo", url="foo"), - response=httpx.Response(status_code=401), - ) - raise e - - with pytest.raises(httpx.HTTPStatusError): - auth.execute_with_token(lambda _: raise_401()) - - verify(auth, times=1)._try_refresh_token() diff --git a/tests/auth/test_public_client_oauth_flow_provider.py b/tests/auth/test_public_client_oauth_flow_provider.py deleted file mode 100644 index 432eeae36..000000000 --- a/tests/auth/test_public_client_oauth_flow_provider.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import httpx -import pytest -from expects import equal -from expects import expect -from mockito import mock -from mockito import unstub -from mockito import when - -from foundry_sdk._core.http_client import HttpClient -from foundry_sdk._core.oauth_utils import PublicClientOAuthFlowProvider - - -@pytest.fixture(name="client", scope="module") -def instantiate_server_oauth_flow_provider(): - return PublicClientOAuthFlowProvider( - client_id="client_id", - redirect_url="redirect_url", - multipass_context_path="/multipass", - scopes=["scope1", "scope2"], - ) - - -@pytest.fixture(scope="module") -def http_client(): - return HttpClient("https://a.b.c.com") - - -def test_get_token(http_client, client): - response = mock(httpx.Response) - when(response).raise_for_status().thenReturn(None) - when(response).json().thenReturn( - {"access_token": "example_token", "expires_in": 42, "token_type": "Bearer"} - ) - - headers = {"Content-Type": "application/x-www-form-urlencoded"} - params = { - "grant_type": "authorization_code", - "code": "code", - "redirect_uri": "redirect_url", - "client_id": "client_id", - "code_verifier": "code_verifier", - "scope": "scope1 scope2 offline_access", - } - - when(http_client).post("/multipass/api/oauth2/token", data=params, headers=headers).thenReturn( - response - ) - token = client.get_token(http_client, code="code", code_verifier="code_verifier") - expect(token.access_token).to(equal("example_token")) - expect(token.token_type).to(equal("Bearer")) - unstub() - - -def test_get_token_throws_when_unsuccessful(http_client, client): - response = mock(httpx.Response) - when(response).raise_for_status().thenRaise( - httpx.HTTPStatusError( - "Foo", - request=httpx.Request("GET", "/foo/bar"), - response=httpx.Response(200), - ), - ) - - headers = {"Content-Type": "application/x-www-form-urlencoded"} - params = { - "grant_type": "authorization_code", - "code": "code", - "redirect_uri": "redirect_url", - "client_id": "client_id", - "code_verifier": "code_verifier", - "scope": "scope1 scope2 offline_access", - } - - when(http_client).post("/multipass/api/oauth2/token", data=params, headers=headers).thenReturn( - response - ) - - with pytest.raises(httpx.HTTPStatusError): - client.get_token(http_client, code="code", code_verifier="code_verifier") - - unstub() - - -def test_refresh_token(http_client, client): - response = mock(httpx.Response) - when(response).raise_for_status().thenReturn(None) - when(response).json().thenReturn( - {"access_token": "example_token", "expires_in": 42, "token_type": "Bearer"} - ) - - headers = {"Content-Type": "application/x-www-form-urlencoded"} - params = { - "grant_type": "refresh_token", - "client_id": "client_id", - "refresh_token": "refresh_token", - } - - when(http_client).post("/multipass/api/oauth2/token", data=params, headers=headers).thenReturn( - response - ) - token = client.refresh_token(http_client, refresh_token="refresh_token") - expect(token.access_token).to(equal("example_token")) - expect(token.token_type).to(equal("Bearer")) - unstub() - - -def test_revoke_token(http_client, client): - response = mock(httpx.Response) - when(response).raise_for_status().thenReturn(None) - when(http_client).post( - "/multipass/api/oauth2/revoke_token", - data={"client_id": "client_id", "token": "token_to_be_revoked"}, - ).thenReturn(response) - client.revoke_token(http_client, "token_to_be_revoked") - unstub() - - -def test_get_scopes(http_client, client): - expect(client.get_scopes()).to(equal(["scope1", "scope2", "offline_access"])) diff --git a/tests/auth/test_user_auth_token_client.py b/tests/auth/test_user_auth_token_client.py deleted file mode 100644 index 0b0b6e9bb..000000000 --- a/tests/auth/test_user_auth_token_client.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import pytest - -from foundry_sdk._core.user_token_auth_client import UserTokenAuth - - -def test_invalid_token_raises_appropriate_error(): - assert pytest.raises(TypeError, lambda: UserTokenAuth()) # type: ignore - assert pytest.raises(TypeError, lambda: UserTokenAuth(1)) # type: ignore - assert pytest.raises(TypeError, lambda: UserTokenAuth(None)) # type: ignore - assert pytest.raises(ValueError, lambda: UserTokenAuth("")) diff --git a/tests/language_models/test_utils.py b/tests/language_models/test_utils.py new file mode 100644 index 000000000..82562f977 --- /dev/null +++ b/tests/language_models/test_utils.py @@ -0,0 +1,177 @@ +# Copyright 2024 Palantir Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import pytest + +from foundry_sdk._core.context_and_environment_vars import HOSTNAME_VAR +from foundry_sdk._core.context_and_environment_vars import TOKEN_VAR +from foundry_sdk._core.http_client import HttpClient +from foundry_sdk.v2.language_models import get_anthropic_base_url +from foundry_sdk.v2.language_models import get_foundry_token +from foundry_sdk.v2.language_models import get_http_client +from foundry_sdk.v2.language_models import get_openai_base_url +from foundry_sdk.v2.language_models.utils import _get_api_gateway_base_url + + +class TestPreviewParameter: + """Test that all functions require preview=True.""" + + def test__get_api_gateway_base_url_requires_preview(self): + with pytest.raises(ValueError, match="preview parameter"): + _get_api_gateway_base_url() + + def test_get_foundry_token_requires_preview(self): + with pytest.raises(ValueError, match="preview parameter"): + get_foundry_token() + + def test_get_openai_base_url_requires_preview(self): + with pytest.raises(ValueError, match="preview parameter"): + get_openai_base_url() + + def test_get_anthropic_base_url_requires_preview(self): + with pytest.raises(ValueError, match="preview parameter"): + get_anthropic_base_url() + + def test_get_http_client_requires_preview(self): + with pytest.raises(ValueError, match="preview parameter"): + get_http_client() + + +class TestGetApiGatewayBaseUrl: + """Test _get_api_gateway_base_url function.""" + + def test_returns_hostname_from_context(self): + token = HOSTNAME_VAR.set("test.palantirfoundry.com") + try: + result = _get_api_gateway_base_url(preview=True) + assert result == "test.palantirfoundry.com" + finally: + HOSTNAME_VAR.reset(token) + + def test_raises_runtime_error_when_not_in_context(self): + with pytest.raises(RuntimeError, match="not available"): + _get_api_gateway_base_url(preview=True) + + +class TestGetFoundryToken: + """Test get_foundry_token function.""" + + def test_returns_token_from_context(self): + token = TOKEN_VAR.set("test-token-12345") + try: + result = get_foundry_token(preview=True) + assert result == "test-token-12345" + finally: + TOKEN_VAR.reset(token) + + def test_raises_runtime_error_when_not_in_context(self): + with pytest.raises(RuntimeError, match="not available"): + get_foundry_token(preview=True) + + +class TestGetOpenaiBaseUrl: + """Test get_openai_base_url function.""" + + def test_returns_correct_url(self): + token = HOSTNAME_VAR.set("test.palantirfoundry.com") + try: + result = get_openai_base_url(preview=True) + assert result == "https://test.palantirfoundry.com/api/v2/llm/proxy/openai/v1" + finally: + HOSTNAME_VAR.reset(token) + + def test_raises_runtime_error_when_not_in_context(self): + with pytest.raises(RuntimeError, match="not available"): + get_openai_base_url(preview=True) + + +class TestGetAnthropicBaseUrl: + """Test get_anthropic_base_url function.""" + + def test_returns_correct_url(self): + token = HOSTNAME_VAR.set("test.palantirfoundry.com") + try: + result = get_anthropic_base_url(preview=True) + assert result == "https://test.palantirfoundry.com/api/v2/llm/proxy/anthropic" + finally: + HOSTNAME_VAR.reset(token) + + def test_raises_runtime_error_when_not_in_context(self): + with pytest.raises(RuntimeError, match="not available"): + get_anthropic_base_url(preview=True) + + +class TestGetHttpClient: + """Test get_http_client function.""" + + def test_returns_http_client(self): + hostname_token = HOSTNAME_VAR.set("test.palantirfoundry.com") + auth_token = TOKEN_VAR.set("test-token-12345") + try: + result = get_http_client(preview=True) + assert isinstance(result, HttpClient) + finally: + HOSTNAME_VAR.reset(hostname_token) + TOKEN_VAR.reset(auth_token) + + def test_injects_auth_header(self): + hostname_token = HOSTNAME_VAR.set("test.palantirfoundry.com") + auth_token = TOKEN_VAR.set("test-token-12345") + try: + client = get_http_client(preview=True) + assert client.headers["Authorization"] == "Bearer test-token-12345" + finally: + HOSTNAME_VAR.reset(hostname_token) + TOKEN_VAR.reset(auth_token) + + def test_merges_auth_header_with_custom_headers(self): + from foundry_sdk._core.config import Config + + hostname_token = HOSTNAME_VAR.set("test.palantirfoundry.com") + auth_token = TOKEN_VAR.set("test-token-12345") + try: + config = Config(default_headers={"X-Custom-Header": "custom-value"}) + client = get_http_client(preview=True, config=config) + assert client.headers["Authorization"] == "Bearer test-token-12345" + assert client.headers["X-Custom-Header"] == "custom-value" + finally: + HOSTNAME_VAR.reset(hostname_token) + TOKEN_VAR.reset(auth_token) + + def test_user_headers_override_auth_header(self): + from foundry_sdk._core.config import Config + + hostname_token = HOSTNAME_VAR.set("test.palantirfoundry.com") + auth_token = TOKEN_VAR.set("test-token-12345") + try: + config = Config(default_headers={"Authorization": "Bearer custom-token"}) + client = get_http_client(preview=True, config=config) + # User-provided auth header should override the auto-injected one + assert client.headers["Authorization"] == "Bearer custom-token" + finally: + HOSTNAME_VAR.reset(hostname_token) + TOKEN_VAR.reset(auth_token) + + def test_raises_runtime_error_when_hostname_not_in_context(self): + with pytest.raises(RuntimeError, match="not available"): + get_http_client(preview=True) + + def test_raises_runtime_error_when_token_not_in_context(self): + hostname_token = HOSTNAME_VAR.set("test.palantirfoundry.com") + try: + with pytest.raises(RuntimeError, match="not available"): + get_http_client(preview=True) + finally: + HOSTNAME_VAR.reset(hostname_token) diff --git a/tests/test_api_client.py b/tests/test_api_client.py deleted file mode 100644 index e4c12b19b..000000000 --- a/tests/test_api_client.py +++ /dev/null @@ -1,663 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import json -import warnings -from datetime import datetime -from datetime import timezone -from typing import Any -from typing import AsyncIterator -from typing import Dict -from typing import List -from typing import Literal -from typing import Optional -from typing import Union -from typing import cast -from unittest.mock import ANY -from unittest.mock import Mock -from unittest.mock import patch - -import httpx -import pytest - -from foundry_sdk._core import ApiClient -from foundry_sdk._core import ApiResponse -from foundry_sdk._core import AsyncApiClient -from foundry_sdk._core import ConfidentialClientAuth -from foundry_sdk._core import Config -from foundry_sdk._core import RequestInfo -from foundry_sdk._core import UserTokenAuth -from foundry_sdk._errors import ApiNotFoundError -from foundry_sdk._errors import BadRequestError -from foundry_sdk._errors import ConflictError -from foundry_sdk._errors import ConnectionError -from foundry_sdk._errors import InternalServerError -from foundry_sdk._errors import NotFoundError -from foundry_sdk._errors import PalantirRPCException -from foundry_sdk._errors import PermissionDeniedError -from foundry_sdk._errors import ProxyError -from foundry_sdk._errors import RateLimitError -from foundry_sdk._errors import ReadTimeout -from foundry_sdk._errors import RequestEntityTooLargeError -from foundry_sdk._errors import ServiceUnavailable -from foundry_sdk._errors import StreamConsumedError -from foundry_sdk._errors import UnauthorizedError -from foundry_sdk._errors import UnprocessableEntityError -from foundry_sdk._errors import WriteTimeout -from tests.server import FooBar -from tests.server import FooData - -HOSTNAME = "localhost:8123" - - -class AttrDict(Dict[str, Any]): - def __init__(self, *args: Any, **kwargs: Any): - super(AttrDict, self).__init__(*args, **kwargs) - self.__dict__ = self - - -EXAMPLE_ERROR = json.dumps( - { - "errorCode": "ERROR_CODE", - "errorName": "ERROR_NAME", - "errorInstanceId": "123", - "parameters": {}, - } -) - -EMPTY_BODY = "" - - -def assert_called_with(client: Union[ApiClient, AsyncApiClient], **kwargs): - if isinstance(client, AsyncApiClient): - build_request = cast(Mock, client._client.build_request) - else: - build_request = cast(Mock, client._session.build_request) - - build_request.assert_called_with( - **{ - "method": ANY, - "url": ANY, - "headers": ANY, - "params": ANY, - "content": ANY, - "timeout": ANY, - **kwargs, - } - ) - - -def _throw(exception: Exception): - def wrapper(*_args, **_kwargs): - raise exception - - return wrapper - - -def get_mock_awaitable(return_value): - async def mock_awaitable(*args, **kwargs): - return return_value - - return Mock(wraps=mock_awaitable) - - -def create_mock_client(config: Optional[Config] = None, hostname=HOSTNAME): - client = ApiClient(auth=UserTokenAuth(token="bar"), hostname=hostname, config=config) - client._session.build_request = Mock(wraps=client._session.build_request) # type: ignore - client._session.send = Mock(return_value=AttrDict(status_code=200, content=b"", headers={})) # type: ignore - return client - - -def create_async_mock_client(config: Optional[Config] = None, hostname=HOSTNAME): - client = AsyncApiClient(auth=UserTokenAuth(token="bar"), hostname=hostname, config=config) - client._client.build_request = Mock(wraps=client._client.build_request) # type: ignore - client._client.send = get_mock_awaitable(AttrDict(status_code=200, content=b"", headers={})) # type: ignore - return client - - -def create_client( - config: Optional[Config] = None, - hostname=HOSTNAME, - scheme: Literal["https", "http"] = "http", -): - config = config or Config() - config.scheme = scheme - return ApiClient(auth=UserTokenAuth(token="bar"), hostname=hostname, config=config) - - -def create_async_client( - config: Optional[Config] = None, - hostname=HOSTNAME, - scheme: Literal["https", "http"] = "http", -): - config = config or Config() - config.scheme = scheme - return AsyncApiClient(auth=UserTokenAuth(token="bar"), hostname=hostname, config=config) - - -def test_authorization_header(): - client = create_mock_client() - client.call_api(RequestInfo.with_defaults("GET", "/foo/bar")) - # Ensure the bearer token gets added to the headers - assert_called_with(client, headers={"Authorization": "Bearer bar"}) - - -def test_timeout(): - client = create_mock_client(config=Config(timeout=60)) - client.call_api(RequestInfo.with_defaults("GET", "/foo/bar", request_timeout=30)) - assert_called_with(client, timeout=30) - - -def test_config_passed_to_http_client(): - # Just check that at least one config var was set correctly to ensure - # the config is being passed to the http client - client = create_client(config=Config(timeout=60)) - assert client._session.timeout == httpx.Timeout(60) - - -def test_path_encoding(): - client = create_mock_client() - - client.call_api( - RequestInfo.with_defaults( - "GET", - "/files/{path}", - path_params={"path": "/my/file.txt"}, - ) - ) - - assert_called_with(client, url="/api/files/%2Fmy%2Ffile.txt") - - -def test_null_query_params(): - client = create_mock_client() - client.call_api( - RequestInfo.with_defaults("GET", "/foo/bar", query_params={"foo": "foo", "bar": None}) - ) - assert_called_with(client, url="/api/foo/bar", params=[("foo", "foo")]) - - -def test_shared_transport(): - client1 = create_mock_client() - client2 = create_mock_client() - session1 = client1._session - session2 = client2._session - assert session1._transport == session2._transport - - -def call_api_helper( - status_code: int, - data: str, - headers: Dict[str, str] = {}, -): - client = ApiClient(auth=UserTokenAuth(token="bar"), hostname="foo") - - client._session.send = Mock( # type: ignore - return_value=AttrDict( - status_code=status_code, - headers=headers, - content=data.encode(), - text=data, - json=lambda: json.loads(data), - ) - ) - - return client.call_api(RequestInfo.with_defaults("POST", "/abc")) - - -def test_call_api_400(): - with pytest.raises(BadRequestError) as info: - call_api_helper(status_code=400, data=EXAMPLE_ERROR, headers={"Header": "A"}) - - assert info.value.name == "ERROR_NAME" - assert info.value.error_instance_id == "123" - assert info.value.parameters == {} - - -def test_401_error(): - with pytest.raises(UnauthorizedError): - call_api_helper(status_code=401, data=EXAMPLE_ERROR) - - -def test_403_error(): - with pytest.raises(PermissionDeniedError): - call_api_helper(status_code=403, data=EXAMPLE_ERROR) - - -def test_404_error(): - with pytest.raises(NotFoundError): - call_api_helper(status_code=404, data=EXAMPLE_ERROR) - - -def test_404_with_no_body(): - with pytest.raises(ApiNotFoundError): - call_api_helper(status_code=404, data=EMPTY_BODY) - - -def test_422_error(): - with pytest.raises(UnprocessableEntityError): - call_api_helper(status_code=422, data=EXAMPLE_ERROR) - - -def test_429_error(): - with pytest.raises(RateLimitError): - call_api_helper(status_code=429, data=EMPTY_BODY) - - -def test_503_with_propagate_to_caller(): - client = ApiClient( - auth=UserTokenAuth(token="bar"), - hostname="foo", - config=Config(propagate_qos="PROPAGATE_429_AND_503_TO_CALLER"), - ) - - client._session.send = Mock( - side_effect=( - AttrDict(status_code=503, headers={}, content=b"", text=""), - AttrDict(status_code=200, headers={}, content=b"", text=""), - ) - ) - - with pytest.raises(ServiceUnavailable): - client.call_api(RequestInfo.with_defaults("POST", "/abc")) - - assert client._session.send.call_count == 1 - - -def test_503_with_retry(): - client = ApiClient( - auth=UserTokenAuth(token="bar"), - hostname="foo", - config=Config(propagate_qos="AUTOMATIC_RETRY"), - ) - - client._session.send = Mock( - side_effect=( - AttrDict(status_code=503, headers={}, content=b"", text=""), - AttrDict(status_code=200, headers={}, content=b"", text=""), - ) - ) - - response = client.call_api(RequestInfo.with_defaults("POST", "/abc", response_mode="RAW")) - assert response.status_code == 200 - - assert client._session.send.call_count == 2 - - -def test_413_error(): - with pytest.raises(RequestEntityTooLargeError): - call_api_helper(status_code=413, data=EXAMPLE_ERROR) - - -def test_409_error(): - with pytest.raises(ConflictError): - call_api_helper(status_code=409, data=EXAMPLE_ERROR) - - -def test_call_api_500(): - with pytest.raises(InternalServerError): - call_api_helper(status_code=500, data=EXAMPLE_ERROR) - - -def test_call_api_599(): - with pytest.raises(InternalServerError): - call_api_helper(status_code=599, data=EXAMPLE_ERROR) - - -def test_call_api_600(): - with pytest.raises(PalantirRPCException): - call_api_helper(status_code=600, data=EXAMPLE_ERROR) - - -def test_cannot_cause_invalid_url_error(): - client = create_client() - request_info = RequestInfo.with_defaults("GET", "/foo/{bar}", path_params={"bar": "|https://"}) - - # This confirms that the path parameters are encoded since "|https://" in a URL is invalid - # The encoded path doesn't exist so we get back a 404 error - with pytest.raises(NotFoundError): - client.call_api(request_info) - - -def test_connect_timeout(): - client = create_client(hostname="localhost:9876", config=Config(timeout=1e-6)) - request_info = RequestInfo.with_defaults("GET", "/foo/bar") - - with pytest.raises(ConnectionError): - client.call_api(request_info) - - -def test_read_timeout(): - client = create_client(config=Config(timeout=1e-6)) - request_info = RequestInfo.with_defaults("GET", "/foo/timeout") - - with pytest.raises(ReadTimeout): - client.call_api(request_info) - - -def test_write_timeout(): - client = create_client(config=Config(timeout=1e-6)) - data = b"*" * 1024 * 1024 * 100 - request_info = RequestInfo.with_defaults("GET", "/foo/timeout", body=data) - - with pytest.raises(WriteTimeout): - client.call_api(request_info) - - -def test_stream_consumed_error(): - client = create_client() - request_info = RequestInfo.with_defaults("GET", "/foo/stream", response_mode="STREAMING") - - with client.call_api(request_info) as response: - for _ in response.iter_bytes(): - pass - - with pytest.raises(StreamConsumedError): - for _ in response.iter_bytes(): - pass - - -def test_streaming_response_type(): - client = create_client() - request_info = RequestInfo.with_defaults("GET", "/foo/stream", response_mode="STREAMING") - - with client.call_api(request_info) as response: - iterator = response.iter_bytes() - assert next(iterator) == b"foo\n" - assert next(iterator) == b"bar\n" - assert next(iterator) == b"baz" - - -def test_raw_response_type(): - client = create_client() - request_info = RequestInfo.with_defaults("GET", "/foo/bar", response_mode="RAW") - - response = client.call_api(request_info) - assert response.text == '{"foo":"foo","bar":2}' - assert response.json() == {"foo": "foo", "bar": 2} - - -def test_iterator_response_type(): - client = create_client() - request_info = RequestInfo.with_defaults( - "GET", - "/foo/iterator", - response_mode="ITERATOR", - response_type=FooData, - ) - - response = client.call_api(request_info) - assert len(response.data) == 2 - assert len(list(response)) == 2 - - -def test_proxy_error(): - client = create_client() - request_info = RequestInfo.with_defaults("GET", "/proxy/error") - - # I can't figure out a way to mock "ProxyError" since it involves connecting to a server - # using https - # https://github.com/encode/httpcore/blob/a1735520e3826ccc861cdadf3e692abfbb19ac6a/httpcore/_sync/http_proxy.py#L156 - # This is an error we could hit so I'll just use the mock library to simulate the error - with patch("httpx.Client.send", side_effect=_throw(httpx.ProxyError("foo"))): - with pytest.raises(ProxyError): - client.call_api(request_info) - - -def test_ssl_error(): - client = create_client(scheme="https", config=Config(timeout=1)) - request_info = RequestInfo.with_defaults("GET", "localhost:8123") - - with pytest.raises(ConnectionError) as error: - client.call_api(request_info) - - assert "SSL" in str(error.value) - - -def test_passing_in_str_auth(): - with pytest.raises(TypeError) as e: - ApiClient(auth="foo", hostname="localhost:8123") # type: ignore - assert str(e.value).startswith( - "auth must be an instance of UserTokenAuth, ConfidentialClientAuth or PublicClientAuth, not a string." - ) - - with pytest.raises(TypeError) as e: - AsyncApiClient(auth="foo", hostname="localhost:8123") # type: ignore - assert str(e.value).startswith( - "auth must be an instance of UserTokenAuth, ConfidentialClientAuth or PublicClientAuth, not a string." - ) - - -def test_passing_in_int_to_auth(): - with pytest.raises(TypeError) as e: - ApiClient(auth=2, hostname="localhost:8123") # type: ignore - assert ( - str(e.value) - == "auth must be an instance of UserTokenAuth, ConfidentialClientAuth or PublicClientAuth, not an instance of int." - ) - - with pytest.raises(TypeError) as e: - AsyncApiClient(auth=2, hostname="localhost:8123") # type: ignore - assert ( - str(e.value) - == "auth must be an instance of UserTokenAuth, ConfidentialClientAuth or PublicClientAuth, not an instance of int." - ) - - -def test_passing_in_int_to_hostname(): - with pytest.raises(TypeError) as e: - ApiClient(auth=UserTokenAuth(token="foo"), hostname=2) # type: ignore - assert str(e.value) == "The hostname must be a string, not ." - - with pytest.raises(TypeError) as e: - AsyncApiClient(auth=UserTokenAuth(token="foo"), hostname=2) # type: ignore - assert str(e.value) == "The hostname must be a string, not ." - - -def test_passing_in_int_to_config(): - with pytest.raises(TypeError) as e: - ApiClient(auth=UserTokenAuth(token="foo"), hostname="localhost:1234", config=2) # type: ignore - assert str(e.value) == "config must be an instance of Config, not ." - - with pytest.raises(TypeError) as e: - AsyncApiClient(auth=UserTokenAuth(token="foo"), hostname="localhost:1234", config=2) # type: ignore - assert str(e.value) == "config must be an instance of Config, not ." - - -def test_config_shared_with_auth(): - config = Config(timeout=1) - auth = ConfidentialClientAuth(client_id="foo", client_secret="bar") - assert auth._hostname is None - assert auth._config is None - - with warnings.catch_warnings(record=True) as w: - ApiClient(auth=auth, hostname="localhost:1234", config=config) - assert len(w) == 0 - - assert auth._hostname == "localhost:1234" - assert auth._config == config - - -def test_auth_config_prioritized(): - auth_config = Config(timeout=1) - auth = ConfidentialClientAuth( - client_id="foo", client_secret="bar", hostname="localhost:9876", config=auth_config - ) - - with warnings.catch_warnings(record=True) as w: - ApiClient(auth=auth, hostname="localhost:1234", config=Config(timeout=2)) - # No warning because the hostnames are different - assert len(w) == 0 - - # Make sure the ApiClient hostname is prioritized - assert auth._hostname == "localhost:9876" - assert auth._config == auth_config - - -def test_duplicate_auth_config_warns(): - hostname = "localhost:1234" - config = Config(timeout=1) - auth = ConfidentialClientAuth( - client_id="foo", client_secret="bar", hostname=hostname, config=config - ) - - with warnings.catch_warnings(record=True) as w: - ApiClient(auth=auth, hostname=hostname, config=config) - # Two warnings because both the hostname and config are the same - assert len(w) == 2 - - # Make sure the ApiClient hostname is prioritized - assert auth._hostname == hostname - assert auth._config == config - - -def test_create_headers(): - client = create_client() - expected_headers = { - "Authorization": "Bearer bar", - "bool_header": "true", - "bytes_header": "bytes".encode("utf-8"), - "datetime_header": "2025-01-01T10:00:00+00:00", - "float_header": "123.123", - "int_header": "123", - "str_header": "string", - } - assert expected_headers == client._create_headers( - request_info=RequestInfo.with_defaults( - "GET", - "/files/{path}", - header_params={ - "bool_header": True, - "bytes_header": "bytes".encode("utf-8"), - "datetime_header": datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc), - "float_header": 123.123, - "int_header": 123, - "str_header": "string", - "optional_header": None, - }, - ), - token=UserTokenAuth(token="bar").get_token(), - ) - - -def test_response_decode_bytes(): - response = ApiResponse( - RequestInfo.with_defaults("GET", "/foo/bar", response_type=bytes), - httpx.Response(200, content=b"foo"), - ) - - assert response.decode() == b"foo" - - -def test_response_decode_present_optional_bytes(): - response = ApiResponse( - RequestInfo.with_defaults("GET", "/foo/bar", response_type=Optional[bytes]), - httpx.Response(200, content=b"foo"), - ) - - assert response.decode() == b"foo" - - -def test_response_decode_empty_optional_bytes(): - response = ApiResponse( - RequestInfo.with_defaults("GET", "/foo/bar", response_type=Optional[bytes]), - httpx.Response(200, content=b""), - ) - - assert response.decode() is None - - -@pytest.mark.asyncio(scope="session") -async def test_async_headers(): - client = create_async_mock_client() - await client.call_api(RequestInfo.with_defaults("GET", "/foo", header_params={"Foo": "Bar"})) - assert_called_with(client, headers={"Authorization": "Bearer bar", "Foo": "Bar"}) - - -@pytest.mark.asyncio(scope="session") -async def test_async_timeout(): - client = create_async_mock_client(config=Config(timeout=60)) - await client.call_api(RequestInfo.with_defaults("GET", "/foo/bar", request_timeout=30)) - assert_called_with(client, timeout=30) - - -@pytest.mark.asyncio(scope="session") -async def test_async_path(): - client = create_async_mock_client() - await client.call_api(RequestInfo.with_defaults("GET", "/files")) - assert_called_with(client, url="/api/files") - - -@pytest.mark.asyncio(scope="session") -async def test_async_query_params(): - client = create_async_mock_client() - await client.call_api(RequestInfo.with_defaults("GET", "/foo", query_params={"foo": "bar"})) - assert_called_with(client, url="/api/foo", params=[("foo", "bar")]) - - -@pytest.mark.asyncio(scope="session") -async def test_async_default_raw_response_type(): - client = create_async_client() - request_info = RequestInfo.with_defaults( - "GET", "/foo/bar", response_mode="DECODED", response_type=FooBar - ) - - response = await client.call_api(request_info) - assert response == FooBar(foo="foo", bar=2) - - -@pytest.mark.asyncio(scope="session") -async def test_async_streaming_response_type(): - client = create_async_client() - request_info = RequestInfo.with_defaults("GET", "/foo/stream", response_mode="STREAMING") - - async with client.call_api(request_info) as response: - iterator = response.aiter_bytes() - assert await iterator.__anext__() == b"foo\n" - assert await iterator.__anext__() == b"bar\n" - assert await iterator.__anext__() == b"baz" - - -@pytest.mark.asyncio(scope="session") -async def test_async_raw_response_type(): - client = create_async_client() - request_info = RequestInfo.with_defaults("GET", "/foo/bar", response_mode="RAW") - - response = await client.call_api(request_info) - assert response.text == '{"foo":"foo","bar":2}' - assert response.json() == {"foo": "foo", "bar": 2} - - -async def collect_async_iterator(aiterator: AsyncIterator[Any]) -> List[Any]: - """Collects the items from an async iterator into a list.""" - result = [] - async for item in aiterator: - result.append(item) - return result - - -@pytest.mark.asyncio(scope="session") -async def test_async_iterator_response_type(): - client = create_async_client() - request_info = RequestInfo.with_defaults( - "GET", - "/foo/iterator", - response_mode="ITERATOR", - response_type=FooData, - ) - - response = client.call_api(request_info) - assert len(await response._page_iterator.get_data()) == 2 - assert len(await collect_async_iterator(response)) == 2 diff --git a/tests/test_body_serialization.py b/tests/test_body_serialization.py deleted file mode 100644 index 0bf59598c..000000000 --- a/tests/test_body_serialization.py +++ /dev/null @@ -1,508 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import json -from datetime import datetime -from datetime import timedelta -from datetime import timezone -from typing import Any -from typing import Dict -from typing import List -from typing import Literal -from typing import Optional - -import pydantic -import pytest - -from foundry_sdk._core.api_client import BaseApiClient - - -# Test models for serialization -class SimpleModel(pydantic.BaseModel): - name: str - count: int - - -class ComplexModel(pydantic.BaseModel): - id: str - date: datetime - nested: SimpleModel - tags: List[str] = [] - optional_field: Optional[int] = None - - -class ModelWithArray(pydantic.BaseModel): - id: str - items: List[SimpleModel] - - -class ModelWithOptionalArray(pydantic.BaseModel): - id: str - items: Optional[List[SimpleModel]] = None - - -class ModelWithNestedArrays(pydantic.BaseModel): - id: str - matrix: List[List[SimpleModel]] - - -# Create a test client for serialization tests -class TestApiClient(BaseApiClient): - """A minimal implementation for testing the serialization logic""" - - def __init__(self): - pass # Skip the standard initialization - - -# Helper function to serialize and then deserialize back to verify correctness -def serialize_and_deserialize(client: TestApiClient, data: Any) -> Any: - """ - Serialize data using the client's _serialize method and then deserialize it back to a generic Python object (e.g., dict or list). - Note: This does NOT reconstruct the original input type if it was a custom class. - """ - serialized = client._serialize(data) - # If None is returned, it means the data was None - if serialized is None: - return None - # Decode the bytes and parse as JSON - return json.loads(serialized.decode("utf-8")) - - -# ----- Basic Serialization Tests ----- - - -def test_serialize_none(): - """Test serializing None value.""" - client = TestApiClient() - result = client._serialize(None) - assert result is None - - -def test_serialize_bytes(): - """Test that bytes are passed through as-is.""" - client = TestApiClient() - raw_bytes = b"raw byte content" - result = client._serialize(raw_bytes) - assert result is raw_bytes # Should return the same bytes object - - -def test_serialize_primitive_types(): - """Test serializing primitive JSON types.""" - client = TestApiClient() - - # String - assert json.loads((client._serialize("test string") or b"").decode()) == "test string" - - # Number - assert json.loads((client._serialize(42) or b"").decode()) == 42 - assert json.loads((client._serialize(3.14) or b"").decode()) == 3.14 - - # Boolean - assert json.loads((client._serialize(True) or b"").decode()) - assert not json.loads((client._serialize(False) or b"").decode()) - - # Array of primitives - assert json.loads((client._serialize([1, 2, 3]) or b"").decode()) == [1, 2, 3] - - # Object of primitives - assert json.loads((client._serialize({"key": "value"}) or b"").decode()) == {"key": "value"} - - -# ----- Pydantic BaseModel Serialization Tests ----- - - -def test_serialize_simple_model(): - """Test serializing a single simple Pydantic model.""" - client = TestApiClient() - model = SimpleModel(name="test", count=42) - - result = serialize_and_deserialize(client, model) - assert result == {"name": "test", "count": 42} - - -def test_serialize_complex_model(): - """Test serializing a complex Pydantic model with nested objects.""" - client = TestApiClient() - - now = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc) - model = ComplexModel( - id="test-id", date=now, nested=SimpleModel(name="nested", count=10), tags=["tag1", "tag2"] - ) - - result = serialize_and_deserialize(client, model) - assert result == { - "id": "test-id", - "date": "2023-01-01T12:00:00+00:00", - "nested": {"name": "nested", "count": 10}, - "tags": ["tag1", "tag2"], - } - - # Optional fields that are None should be excluded - assert "optional_field" not in result - - -def test_serialize_model_with_none_fields(): - """Test that None fields are properly excluded from serialization.""" - client = TestApiClient() - - model = ComplexModel( - id="test-id", - date=datetime(2023, 1, 1, tzinfo=timezone.utc), - nested=SimpleModel(name="nested", count=10), - optional_field=None, - ) - - result = serialize_and_deserialize(client, model) - assert "optional_field" not in result - - -# ----- Array Serialization Tests ----- - - -def test_serialize_array_of_models(): - """Test serializing an array of Pydantic models.""" - client = TestApiClient() - - models = [ - SimpleModel(name="item1", count=1), - SimpleModel(name="item2", count=2), - SimpleModel(name="item3", count=3), - ] - - result = serialize_and_deserialize(client, models) - assert result == [ - {"name": "item1", "count": 1}, - {"name": "item2", "count": 2}, - {"name": "item3", "count": 3}, - ] - - -def test_serialize_empty_array(): - """Test serializing an empty array.""" - client = TestApiClient() - result = serialize_and_deserialize(client, []) - assert result == [] - - -def test_serialize_array_with_none(): - """Test serializing an array containing None values.""" - client = TestApiClient() - - # Array with None values - data = [SimpleModel(name="item1", count=1), None, SimpleModel(name="item3", count=3)] - - result = serialize_and_deserialize(client, data) - assert result == [{"name": "item1", "count": 1}, None, {"name": "item3", "count": 3}] - - -def test_serialize_mixed_array(): - """Test serializing a mixed array with different types including models.""" - client = TestApiClient() - - data = ["string", 42, {"key": "value"}, SimpleModel(name="model", count=1), [1, 2, 3]] - - result = serialize_and_deserialize(client, data) - assert result == ["string", 42, {"key": "value"}, {"name": "model", "count": 1}, [1, 2, 3]] - - -# ----- Nested Structure Serialization Tests ----- - - -def test_serialize_model_with_array(): - """Test serializing a model that contains an array of models.""" - client = TestApiClient() - - model = ModelWithArray( - id="test-id", items=[SimpleModel(name="item1", count=1), SimpleModel(name="item2", count=2)] - ) - - result = serialize_and_deserialize(client, model) - assert result == { - "id": "test-id", - "items": [{"name": "item1", "count": 1}, {"name": "item2", "count": 2}], - } - - -def test_serialize_model_with_optional_array_present(): - """Test serializing a model with an optional array that is present.""" - client = TestApiClient() - - model = ModelWithOptionalArray(id="test-id", items=[SimpleModel(name="item", count=1)]) - - result = serialize_and_deserialize(client, model) - assert result == {"id": "test-id", "items": [{"name": "item", "count": 1}]} - - -def test_serialize_model_with_optional_array_none(): - """Test serializing a model with an optional array that is None.""" - client = TestApiClient() - - model = ModelWithOptionalArray(id="test-id") # items defaults to None - - result = serialize_and_deserialize(client, model) - assert result == {"id": "test-id"} - assert "items" not in result - - -def test_serialize_model_with_nested_arrays(): - """Test serializing a model with nested arrays of models.""" - client = TestApiClient() - - model = ModelWithNestedArrays( - id="test-id", - matrix=[ - [SimpleModel(name="1,1", count=11), SimpleModel(name="1,2", count=12)], - [SimpleModel(name="2,1", count=21), SimpleModel(name="2,2", count=22)], - ], - ) - - result = serialize_and_deserialize(client, model) - assert result == { - "id": "test-id", - "matrix": [ - [{"name": "1,1", "count": 11}, {"name": "1,2", "count": 12}], - [{"name": "2,1", "count": 21}, {"name": "2,2", "count": 22}], - ], - } - - -def test_serialize_deeply_nested_structure(): - """Test serializing a deeply nested structure with models at various levels.""" - client = TestApiClient() - - data = { - "top_level": SimpleModel(name="top", count=1), - "nested": { - "model": SimpleModel(name="nested", count=2), - "list": [SimpleModel(name="list1", count=3), SimpleModel(name="list2", count=4)], - }, - "matrix": [ - [SimpleModel(name="m11", count=11), SimpleModel(name="m12", count=12)], - [SimpleModel(name="m21", count=21), SimpleModel(name="m22", count=22)], - ], - "mixed": [ - {"model": SimpleModel(name="mixed", count=5)}, - [SimpleModel(name="array", count=6)], - ], - } - - result = serialize_and_deserialize(client, data) - assert result == { - "top_level": {"name": "top", "count": 1}, - "nested": { - "model": {"name": "nested", "count": 2}, - "list": [{"name": "list1", "count": 3}, {"name": "list2", "count": 4}], - }, - "matrix": [ - [{"name": "m11", "count": 11}, {"name": "m12", "count": 12}], - [{"name": "m21", "count": 21}, {"name": "m22", "count": 22}], - ], - "mixed": [{"model": {"name": "mixed", "count": 5}}, [{"name": "array", "count": 6}]], - } - - -# ----- Dictionary Serialization Tests ----- - - -def test_serialize_dict_with_model_values(): - """Test serializing a dictionary with model values.""" - client = TestApiClient() - - data = { - "model1": SimpleModel(name="first", count=1), - "model2": SimpleModel(name="second", count=2), - } - - result = serialize_and_deserialize(client, data) - assert result == { - "model1": {"name": "first", "count": 1}, - "model2": {"name": "second", "count": 2}, - } - - -def test_serialize_dict_with_mixed_values(): - """Test serializing a dictionary with a mix of model and non-model values.""" - client = TestApiClient() - - data = { - "model": SimpleModel(name="model", count=1), - "string": "text value", - "number": 42, - "boolean": True, - "array": [1, 2, 3], - "nested_array": [SimpleModel(name="nested", count=2)], - } - - result = serialize_and_deserialize(client, data) - assert result == { - "model": {"name": "model", "count": 1}, - "string": "text value", - "number": 42, - "boolean": True, - "array": [1, 2, 3], - "nested_array": [{"name": "nested", "count": 2}], - } - - -def test_serialize_dict_with_nested_dicts(): - """Test serializing a dictionary with nested dictionaries containing models.""" - client = TestApiClient() - - data = {"level1": {"level2": {"model": SimpleModel(name="deeply_nested", count=42)}}} - - result = serialize_and_deserialize(client, data) - assert result == {"level1": {"level2": {"model": {"name": "deeply_nested", "count": 42}}}} - - -# ----- Special Cases and Edge Cases ----- - - -def test_serialize_model_with_alias(): - """Test serializing a model with field aliases.""" - client = TestApiClient() - - class ModelWithAlias(pydantic.BaseModel): - user_id: str = pydantic.Field(alias="userId") - created_at: datetime = pydantic.Field(alias="createdAt") - - model = ModelWithAlias(userId="test-user", createdAt=datetime(2023, 1, 1, tzinfo=timezone.utc)) - - result = serialize_and_deserialize(client, model) - # Should use the aliases in the output JSON - assert "userId" in result - assert "createdAt" in result - assert "user_id" not in result - assert "created_at" not in result - - -def test_serialize_cyclic_references(): - """Test that serializing cyclic references raises an exception.""" - client = TestApiClient() - - # Create a cyclic reference - a = {} - b = {"a": a} - a["b"] = b - - with pytest.raises((TypeError, ValueError)): - client._serialize(a) - - -def test_serialize_datetime_values(): - """Test serializing datetime values in models.""" - client = TestApiClient() - - # UTC datetime - utc_dt = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc) - model = ComplexModel(id="test", date=utc_dt, nested=SimpleModel(name="test", count=1)) - - result = serialize_and_deserialize(client, model) - assert result["date"] == "2023-01-01T12:00:00+00:00" - - # Non-UTC datetime - est_tz = timezone(timedelta(hours=-5)) - est_dt = datetime(2023, 1, 1, 7, 0, 0, tzinfo=est_tz) - model = ComplexModel(id="test", date=est_dt, nested=SimpleModel(name="test", count=1)) - - result = serialize_and_deserialize(client, model) - # Should be normalized to UTC in ISO format - assert result["date"] == "2023-01-01T12:00:00+00:00" - - -def test_serialize_real_world_complex_payload(): - """Test serializing a complex real-world-like request payload.""" - client = TestApiClient() - - # Create a complex nested payload similar to what might be used in a real API - class Address(pydantic.BaseModel): - street: str - city: str - postal_code: str - country: str - - class Contact(pydantic.BaseModel): - email: str - phone: Optional[str] = None - - class User(pydantic.BaseModel): - id: str - name: str - address: Address - contacts: List[Contact] - is_active: bool = True - created_at: datetime - last_login: Optional[datetime] = None - preferences: Dict[str, Any] = {} - - class OrderItem(pydantic.BaseModel): - product_id: str = pydantic.Field(alias="productId") - quantity: int - unit_price: float = pydantic.Field(alias="unitPrice") - - class Order(pydantic.BaseModel): - id: str - user: User - items: List[OrderItem] - total_amount: float = pydantic.Field(alias="totalAmount") - status: Literal["pending", "processing", "shipped", "delivered"] - shipping_address: Optional[Address] = pydantic.Field(alias="shippingAddress", default=None) - - # Create an instance with deeply nested structure - order = Order( - id="order-123", - user=User( - id="user-456", - name="John Doe", - address=Address( - street="123 Main St", city="Anytown", postal_code="12345", country="USA" - ), - contacts=[ - Contact(email="john@example.com", phone="+1234567890"), - Contact(email="johndoe@work.com"), - ], - created_at=datetime(2022, 1, 1, tzinfo=timezone.utc), - preferences={"theme": "dark", "notifications": True}, - ), - items=[ - OrderItem(productId="prod-1", quantity=2, unitPrice=29.99), - OrderItem(productId="prod-2", quantity=1, unitPrice=49.99), - ], - totalAmount=109.97, - status="processing", - shippingAddress=Address( - street="456 Shipping Ave", city="Shipville", postal_code="54321", country="USA" - ), - ) - - result = serialize_and_deserialize(client, order) - - # Validate the structure and content of the serialized data - assert result["id"] == "order-123" - assert result["user"]["name"] == "John Doe" - assert result["user"]["address"]["city"] == "Anytown" - assert result["user"]["contacts"][0]["email"] == "john@example.com" - assert result["user"]["contacts"][1].get("phone") is None - assert result["items"][0]["productId"] == "prod-1" - assert result["totalAmount"] == 109.97 - assert result["status"] == "processing" - assert result["shippingAddress"]["street"] == "456 Shipping Ave" - - # Check that aliases are properly used - assert "productId" in result["items"][0] - assert "product_id" not in result["items"][0] - assert "totalAmount" in result - assert "total_amount" not in result - assert "shippingAddress" in result - assert "shipping_address" not in result diff --git a/tests/test_client_init_helpers.py b/tests/test_client_init_helpers.py deleted file mode 100644 index 57c472736..000000000 --- a/tests/test_client_init_helpers.py +++ /dev/null @@ -1,154 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import os -from contextvars import ContextVar -from typing import Optional -from unittest.mock import patch - -from expects import equal -from expects import expect -from expects import raise_error - -from foundry_sdk._core.client_init_helpers import ( - get_hostname_from_context_or_environment_vars, -) # NOQA -from foundry_sdk._core.client_init_helpers import ( - get_user_token_auth_from_context_or_environment_vars, -) # NOQA -from foundry_sdk._core.context_and_environment_vars import HOSTNAME_ENV_VAR -from foundry_sdk._core.context_and_environment_vars import HOSTNAME_VAR -from foundry_sdk._core.context_and_environment_vars import TOKEN_ENV_VAR -from foundry_sdk._core.context_and_environment_vars import TOKEN_VAR -from foundry_sdk._core.context_and_environment_vars import _maybe_get_environment_var -from foundry_sdk._core.context_and_environment_vars import maybe_get_context_var -from foundry_sdk._core.context_and_environment_vars import ( - maybe_get_value_from_context_or_environment_vars, -) # NOQA -from foundry_sdk._errors.environment_not_configured import EnvironmentNotConfigured - -CONTEXT_VAR1: ContextVar[Optional[str]] = ContextVar("CONTEXT VAR1", default=None) -CONTEXT_VAR2: ContextVar[Optional[str]] = ContextVar("CONTEXT VAR2", default=None) - - -def test_maybe_get_context_var(): - example_context_vars = [CONTEXT_VAR1, CONTEXT_VAR2] - - CONTEXT_VAR2.set("context_var 2") - expect(maybe_get_context_var(context_vars=example_context_vars)).to(equal("context_var 2")) - CONTEXT_VAR2.set(None) - - CONTEXT_VAR1.set("context_var 1") - CONTEXT_VAR2.set("context_var 2") - expect(maybe_get_context_var(context_vars=example_context_vars)).to(equal("context_var 1")) - CONTEXT_VAR1.set(None) - CONTEXT_VAR2.set(None) - - -def test_maybe_get_environment_var(): - example_env_vars = ["ENV VAR1", "ENV VAR2", "ENV VAR3"] - - with patch.dict(os.environ, {"ENV VAR3": "environment_var 3"}): - expect(_maybe_get_environment_var(env_vars=example_env_vars)).to(equal("environment_var 3")) - with patch.dict(os.environ, {"ENV VAR1": "environment_var 1", "ENV VAR3": "environment_var 3"}): - expect(_maybe_get_environment_var(env_vars=example_env_vars)).to(equal("environment_var 1")) - - -def test_get_value_from_context_or_env(): - # Test case 1: Context variable is set - CONTEXT_VAR1.set("context_var") - expect( - maybe_get_value_from_context_or_environment_vars( - context_vars=[CONTEXT_VAR1], env_vars=["ENV_VAR_NAME"] - ) - ).to(equal("context_var")) - CONTEXT_VAR1.set(None) - - # Test case 2: Context variable is not set and FOUNDRY_HOSTNAME environment variable is set - with patch.dict(os.environ, {"ENV_VAR_NAME": "environment_var"}): - expect( - maybe_get_value_from_context_or_environment_vars( - context_vars=[CONTEXT_VAR1], env_vars=["ENV_VAR_NAME"] - ) - ).to(equal("environment_var")) - - # Test case 3: Both Context variable and environment variable are not set - expect( - maybe_get_value_from_context_or_environment_vars( - context_vars=[CONTEXT_VAR1], env_vars=["ENV_VAR_NAME"] - ) - ).to(equal(None)) - - # Test case 4: Test context vars are used before env vars - CONTEXT_VAR1.set("context_var") - with patch.dict(os.environ, {"ENV_VAR_NAME": "environment_var"}): - expect( - maybe_get_value_from_context_or_environment_vars( - context_vars=[CONTEXT_VAR1], env_vars=["ENV_VAR_NAME"] - ) - ).to(equal("context_var")) - CONTEXT_VAR1.set(None) - - -def test_get_hostname_from_context_or_environment_vars(): - # Test case 1: Context variable is set - HOSTNAME_VAR.set("hostname_context_var") - expect(get_hostname_from_context_or_environment_vars()).to(equal("hostname_context_var")) - HOSTNAME_VAR.set(None) - - # Test case 2: Context variable is not set and environment variable is set - with patch.dict(os.environ, {HOSTNAME_ENV_VAR: "hostname_environment_var"}): - expect(get_hostname_from_context_or_environment_vars()).to( - equal("hostname_environment_var") - ) - - # Test case 3: Both Context variable and environment variable are not set - expect(lambda: get_hostname_from_context_or_environment_vars()).to( - raise_error(EnvironmentNotConfigured) - ) - - # Test case 4: Test Context variables are used before environment variables - HOSTNAME_VAR.set("hostname_context_var") - with patch.dict(os.environ, {HOSTNAME_ENV_VAR: "hostname_environment_var"}): - expect(get_hostname_from_context_or_environment_vars()).to(equal("hostname_context_var")) - HOSTNAME_VAR.set(None) - - -def test_get_user_token_auth_from_context_or_environment_vars(): - # Test case 1: Context variable is set - TOKEN_VAR.set("user_token_context_var") - expect(get_user_token_auth_from_context_or_environment_vars().get_token().access_token).to( - equal("user_token_context_var") - ) - TOKEN_VAR.set(None) - - # Test case 2: Context variable is not set and environment variable is set - with patch.dict(os.environ, {TOKEN_ENV_VAR: "user_token_environment_var"}): - expect(get_user_token_auth_from_context_or_environment_vars().get_token().access_token).to( - equal("user_token_environment_var") - ) - - # Test case 3: Both Context variable and environment variable are not set - expect(lambda: get_user_token_auth_from_context_or_environment_vars()).to( - raise_error(EnvironmentNotConfigured) - ) - - # Test case 4: Test Context variables are used before environment variables - TOKEN_VAR.set("user_token_context_var") - with patch.dict(os.environ, {TOKEN_ENV_VAR: "user_token_environment_var"}): - expect(get_user_token_auth_from_context_or_environment_vars().get_token().access_token).to( - equal("user_token_context_var") - ) - TOKEN_VAR.set(None) diff --git a/tests/test_datetime.py b/tests/test_datetime.py deleted file mode 100644 index 02c94e5b4..000000000 --- a/tests/test_datetime.py +++ /dev/null @@ -1,27 +0,0 @@ -from datetime import datetime -from datetime import timezone - -import pydantic -import pytest - - -class Model(pydantic.BaseModel): - datetype: pydantic.AwareDatetime - - -def test_init_fails_without_timezone(): - with pytest.raises(pydantic.ValidationError): - Model(datetype=datetime.now()) - - -def test_validate_python_fails_without_timezone(): - with pytest.raises(pydantic.ValidationError): - Model.model_validate({"datetype": "2022-01-01T00:00:00"}) - - -def test_init_passes_with_timezone(): - Model(datetype=datetime.now(tz=timezone.utc)) - - -def test_validate_python_passes_with_timezone(): - Model.model_validate({"datetype": "2022-01-01T00:00:00Z"}) diff --git a/tests/test_errors.py b/tests/test_errors.py index 8862c5152..aa556478a 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,6 +1,19 @@ -import warnings +# Copyright 2024 Palantir Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + -import pytest +import warnings from foundry_sdk import PalantirRPCException from foundry_sdk._errors.utils import deserialize_error diff --git a/tests/test_exception.py b/tests/test_exception.py deleted file mode 100644 index ab559eb18..000000000 --- a/tests/test_exception.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import inspect -import re -from types import ModuleType -from typing import List -from typing import Type - -import pydantic -import pytest - -from foundry_sdk import _errors -from foundry_sdk._errors.sdk_internal_error import PalantirException -from foundry_sdk._errors.sdk_internal_error import SDKInternalError -from foundry_sdk._errors.sdk_internal_error import handle_unexpected - - -def find_exception_subclasses(module: ModuleType) -> List[Type[Exception]]: - exception_subclasses = [] - - # Get all members of the module - members = inspect.getmembers(module) - - for name, obj in members: - # Check if the member is a class - if inspect.isclass(obj): - # Check if the class is a subclass of Exception - if issubclass(obj, Exception): - exception_subclasses.append(obj) - - return exception_subclasses - - -def test_sdk_internal_error(): - with pytest.raises(SDKInternalError) as error: - raise SDKInternalError("test") - - assert ( - re.match( - r"""^test\n -This is an unexpected issue and should be reported. When filing an issue, make sure to copy the package information listed below.\n -OS: \w+ -Python Version: \d+\.\d+\.\d+[^\n]+ -SDK Version: \d+\.\d+\.\d+([\.\-].+)? -OpenAPI Document Version: \d+\.\d+\.\d+([\.\-].+)? -Pydantic Version: \d+\.\d+\.\d+ -Pydantic Core Version: \d+\.\d+\.\d+ -Httpx Version: \d+\.\d+\.\d+ -$""", - str(error.value), - ) - is not None - ), "Mismatch with text: " + str(error.value) - - -def test_handle_unexpected_fails_for_unkonwn_exception(): - @handle_unexpected - def raises_unknown_exception(): - raise ValueError("test") - - with pytest.raises(SDKInternalError) as error: - raises_unknown_exception() - - assert error.value.msg == "test" - - -def test_all_errors_subclass_palantir_exception(): - classes = find_exception_subclasses(_errors) - assert len(classes) >= 5 # sanity check we are finding the classes - for klass in find_exception_subclasses(_errors): - assert issubclass(klass, PalantirException) - - -def test_handle_unexpected_ignores_palantir_exception(): - @handle_unexpected - def raises_known_exception(): - raise PalantirException("Foo") - - with pytest.raises(PalantirException): - raises_known_exception() - - -def test_handle_unexpected_ignores_validation_error(): - class Model(pydantic.BaseModel): - foo: str - - @handle_unexpected - def raises_known_exception(): - Model.model_validate({"foo": 123}) - - with pytest.raises(pydantic.ValidationError): - raises_known_exception() diff --git a/tests/test_http_client.py b/tests/test_http_client.py deleted file mode 100644 index 977ab0e49..000000000 --- a/tests/test_http_client.py +++ /dev/null @@ -1,333 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import os -import ssl -import sys -from typing import Any -from typing import Optional -from typing import Type -from typing import TypeVar -from unittest.mock import patch - -import httpcore -import httpx -import pytest -from httpx._utils import URLPattern - -from foundry_sdk._core.config import Config -from foundry_sdk._core.http_client import AsyncHttpClient -from foundry_sdk._core.http_client import HttpClient -from foundry_sdk._version import __version__ - - -def assert_http_transport(transport: Optional[httpx.BaseTransport]) -> httpx.HTTPTransport: - return assert_isinstance(transport, httpx.HTTPTransport) - - -def assert_async_http_transport( - transport: Optional[httpx.AsyncBaseTransport], -) -> httpx.AsyncHTTPTransport: - return assert_isinstance(transport, httpx.AsyncHTTPTransport) - - -def assert_http_proxy(pool: Optional[httpcore.ConnectionPool]) -> httpcore.HTTPProxy: - return assert_isinstance(pool, httpcore.HTTPProxy) - - -def assert_async_http_proxy( - pool: Optional[httpcore.AsyncConnectionPool], -) -> httpcore.AsyncHTTPProxy: - return assert_isinstance(pool, httpcore.AsyncHTTPProxy) - - -T = TypeVar("T") - - -def assert_isinstance(instance: Any, type: Type[T]) -> T: - if not isinstance(instance, type): - raise Exception(f"Not an instance of {type}", instance) - - return instance - - -def create_client(config: Optional[Config] = None): - config = config or Config() - return HttpClient("localhost:8123", config=config) - - -def create_async_client(config: Optional[Config] = None): - config = config or Config() - return AsyncHttpClient("localhost:8123", config=config) - - -def test_clean_hostname(): - assert HttpClient("http://example.com").base_url.host == "example.com" - assert HttpClient("https://example.com").base_url.host == "example.com" - assert HttpClient("example.com/").base_url.host == "example.com" - assert HttpClient("example.com").base_url.host == "example.com" - - -@pytest.fixture -def tmp_cert(tmp_path): - cert_file = tmp_path / "cert.pem" - cert_file.write_text("cert") - yield cert_file.as_posix() - - -@pytest.fixture -def tmp_cert_dupe(tmp_path): - cert_file = tmp_path / "cert.pem" - cert_file.write_text("cert") - yield cert_file.as_posix() - - -@pytest.fixture -def patch_ssl_verify(): - with patch.object(ssl.SSLContext, "load_verify_locations") as mock_method: - # You can specify a return value or a side effect if needed - mock_method.return_value = None # Example: make it do nothing - yield mock_method - - -@pytest.fixture -def temp_os_env(): - old_environ = os.environ.copy() - - # Make sure to start with a clean slate - for key in ["REQUESTS_CA_BUNDLE", "SSL_CERT_FILE"]: - if key in os.environ: - os.environ.pop(key) - - yield os.environ - os.environ = old_environ - - -def test_requests_env_var(temp_os_env, patch_ssl_verify, tmp_cert: str): - temp_os_env["REQUESTS_CA_BUNDLE"] = tmp_cert - assert create_client(Config(verify=True))._verify == tmp_cert - - -def test_ssl_cert_file_env_var(temp_os_env, patch_ssl_verify, tmp_cert: str): - temp_os_env["SSL_CERT_FILE"] = tmp_cert - assert create_client(Config(verify=True))._verify == tmp_cert - - -def test_verify_false_env_var(temp_os_env, patch_ssl_verify, tmp_cert: str): - temp_os_env["REQUESTS_CA_BUNDLE"] = tmp_cert - assert not create_client(Config(verify=False))._verify - - -def test_cert_path_takes_precedence(temp_os_env, patch_ssl_verify, tmp_cert: str, tmp_cert_dupe): - temp_os_env["REQUESTS_CA_BUNDLE"] = tmp_cert - assert create_client(Config(verify=tmp_cert_dupe))._verify == tmp_cert_dupe - - -def test_default_headers(): - """Test that the user agent is set correctly.""" - client = create_client() - assert client.headers == { - "Accept-Encoding": "gzip, deflate", - "Accept": "*/*", - "Connection": "keep-alive", - "User-Agent": f"python-foundry-platform-sdk/{__version__} python/3.{sys.version_info.minor}", - } - - """Test that additional headers can be added.""" - client = create_client(Config(default_headers={"Foo": "Bar"})) - assert client.headers == { - "Accept-Encoding": "gzip, deflate", - "Accept": "*/*", - "Connection": "keep-alive", - "Foo": "Bar", - "User-Agent": f"python-foundry-platform-sdk/{__version__} python/3.{sys.version_info.minor}", - } - - -def test_proxies(): - client = create_client(Config(proxies={"https": "https://foo.bar", "http": "http://foo.bar"})) - - transport = assert_http_transport(client._mounts[URLPattern("https://")]) - proxy = assert_http_proxy(transport._pool) - assert proxy._ssl_context is not None - assert proxy._ssl_context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - assert proxy._proxy_ssl_context is not None - assert proxy._proxy_ssl_context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - assert proxy._proxy_url.scheme == b"https" - assert proxy._proxy_url.host == b"foo.bar" - - transport = assert_http_transport(client._mounts[URLPattern("http://")]) - proxy = assert_http_proxy(transport._pool) - assert proxy._ssl_context is not None - assert proxy._ssl_context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - assert proxy._proxy_ssl_context is None - assert proxy._proxy_url.scheme == b"http" - assert proxy._proxy_url.host == b"foo.bar" - - -def test_bad_proxy_url(): - with pytest.raises(ValueError): - create_client(Config(proxies={"https": "htts://foo.bar"})) - - -def test_timeout(): - client = create_client(config=Config(timeout=60)) - assert client.timeout == httpx.Timeout(60) - - -def test_verify_configures_transport(): - client = create_client() - transport = assert_http_transport(client._transport) - pool = assert_isinstance(transport._pool, httpcore.ConnectionPool) - - assert pool._ssl_context is not None - assert pool._ssl_context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - - client = create_client(Config(verify=False)) - transport = assert_http_transport(client._transport) - pool = assert_isinstance(transport._pool, httpcore.ConnectionPool) - - assert pool._ssl_context is not None - assert pool._ssl_context.verify_mode == ssl.VerifyMode.CERT_NONE - - -def test_default_params(): - client = create_client(Config(default_params={"foo": "bar"})) - assert client.params._dict == {"foo": ["bar"]} - - -def test_scheme(): - client = create_client() - assert str(client.base_url) == "https://localhost:8123" - - client = create_client(Config(scheme="http")) - assert str(client.base_url) == "http://localhost:8123" - - -def test_async_headers(): - client = create_async_client(Config(default_headers={"Foo": "Bar", "User-Agent": "Baz"})) - assert client.headers == { - "Accept-Encoding": "gzip, deflate", - "Accept": "*/*", - "Connection": "keep-alive", - "Foo": "Bar", - "User-Agent": "Baz", - } - - -def test_async_bad_proxy_url(): - with pytest.raises(ValueError): - create_async_client(Config(proxies={"https": "htts://foo.bar"})) - - -def test_async_timeout(): - client = create_async_client(config=Config(timeout=60)) - assert client.timeout == httpx.Timeout(60) - - -def test_async_verify_configures_transport(): - client = create_async_client() - transport = assert_async_http_transport(client._transport) - pool = assert_isinstance(transport._pool, httpcore.AsyncConnectionPool) - - assert pool._ssl_context is not None - assert pool._ssl_context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - - client = create_async_client(Config(verify=False)) - transport = assert_async_http_transport(client._transport) - pool = assert_isinstance(transport._pool, httpcore.AsyncConnectionPool) - - assert pool._ssl_context is not None - assert pool._ssl_context.verify_mode == ssl.VerifyMode.CERT_NONE - - -def test_async_default_params(): - client = create_async_client(Config(default_params={"foo": "bar"})) - assert client.params._dict == {"foo": ["bar"]} - - -def test_async_scheme(): - client = create_async_client() - assert str(client.base_url) == "https://localhost:8123" - - client = create_client(Config(scheme="http")) - assert str(client.base_url) == "http://localhost:8123" - - -def test_attribution_header_present(): - """Test that attribution header is added when context var is set.""" - from foundry_sdk._core.context_and_environment_vars import ATTRIBUTION_VAR - - # Save the original value to restore after test - original_value = ATTRIBUTION_VAR.get() - - try: - # Set attribution value - ATTRIBUTION_VAR.set(["test-attribution-source"]) - - # Create client and check headers - client = create_client() - assert "attribution" in client.headers - assert client.headers["attribution"] == "test-attribution-source" - - # Test with multiple attribution values - ATTRIBUTION_VAR.set(["source1", "source2"]) - client = create_client() - assert client.headers["attribution"] == "source1, source2" - finally: - # Restore original value - ATTRIBUTION_VAR.set(original_value) - - -def test_attribution_header_not_present(): - """Test that attribution header is not added when context var is None.""" - from foundry_sdk._core.context_and_environment_vars import ATTRIBUTION_VAR - - # Save the original value to restore after test - original_value = ATTRIBUTION_VAR.get() - - try: - # Set attribution value to None - ATTRIBUTION_VAR.set(None) - - # Create client and check headers - client = create_client() - assert "attribution" not in client.headers - finally: - # Restore original value - ATTRIBUTION_VAR.set(original_value) - - -def test_async_proxies(): - client = create_async_client( - Config(proxies={"https": "https://foo.bar", "http": "http://foo.bar"}) - ) - - transport = assert_async_http_transport(client._mounts[URLPattern("https://")]) - proxy = assert_async_http_proxy(transport._pool) - assert proxy._ssl_context is not None - assert proxy._ssl_context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - assert proxy._proxy_ssl_context is not None - assert proxy._proxy_ssl_context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - assert proxy._proxy_url.scheme == b"https" - assert proxy._proxy_url.host == b"foo.bar" - - transport = assert_async_http_transport(client._mounts[URLPattern("http://")]) - proxy = assert_async_http_proxy(transport._pool) - assert proxy._ssl_context is not None - assert proxy._ssl_context.verify_mode == ssl.VerifyMode.CERT_REQUIRED - assert proxy._proxy_ssl_context is None - assert proxy._proxy_url.scheme == b"http" - assert proxy._proxy_url.host == b"foo.bar" diff --git a/tests/test_model_base.py b/tests/test_model_base.py deleted file mode 100644 index 5ac4eff5e..000000000 --- a/tests/test_model_base.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple - -import pytest - -from foundry_sdk._core import ModelBase - - -def test_can_be_used_as_dict_key() -> None: - """Test that ModelBase objects can be used as dictionary keys.""" - - class TestModel(ModelBase): - name: str - age: int - - model1 = TestModel(name="Alice", age=30) - model2 = TestModel(name="Alice", age=30) # Same data as model1 - model3 = TestModel(name="Bob", age=25) # Different data - - # Models with same content should be equal - assert model1 == model2 - assert model1 != model3 - - # Models with same content should have same hash - assert hash(model1) == hash(model2) - assert hash(model1) != hash(model3) - - # Use models as dict keys - data = {} - data[model1] = "First model" - data[model3] = "Third model" - - # Should retrieve by equivalent object - assert data[model2] == "First model" - assert len(data) == 2 - - -def test_hash_value_cached() -> None: - """Test that hash value is calculated only once and cached.""" - - class SimpleModel(ModelBase): - value: str - - model = SimpleModel(value="test") - - # Access hash first time - should calculate - hash1 = hash(model) - - # Access hash second time - should use cached value - hash2 = hash(model) - - assert hash1 == hash2 - assert model._hash_called is True - assert model._hash_value == hash1 - - -def test_warns_on_mutation_after_hash() -> None: - """Test that a warning is issued when a model is modified after being hashed.""" - - class MutableModel(ModelBase): - value: str - count: int = 0 - - model = MutableModel(value="original") - - # Use as dict key to trigger hash calculation - data = {model: "some value"} - - # Should warn when modified after hash - with pytest.warns( - UserWarning, match="Modifying MutableModel after it has been used as a dictionary key" - ): - model.value = "changed" - - # Hash value should be reset - assert model._hash_value is None - - # Re-hashing should work - _new_hash = hash(model) - - # But we can add it back - data[model] = "updated value" - assert data[model] == "updated value" - - -def test_hash_includes_class_identity() -> None: - """Test that models with identical attributes but different classes have different hashes.""" - - class UserModel(ModelBase): - name: str - age: int - - class PersonModel(ModelBase): - name: str - age: int - - user = UserModel(name="Alice", age=30) - person = PersonModel(name="Alice", age=30) - - # Despite having identical attributes, hashes should differ due to class identity - assert hash(user) != hash(person) - - # Dictionaries should treat them as separate keys - data: dict[ModelBase, str] = {} - data[user] = "User data" - data[person] = "Person data" - - assert len(data) == 2 - assert data[user] == "User data" - assert data[person] == "Person data" - - -def test_hash_with_different_data_structures() -> None: - """Test that models with different data structures hash correctly.""" - - class StructuredModel(ModelBase): - tuple_field: Tuple[str, int] - list_field: List[str] - dict_field: Dict[str, Any] - set_field: Set[int] - - model1 = StructuredModel( - tuple_field=("hello", 42), - list_field=["a", "b", "c"], - dict_field={"key1": "value1", "key2": 123}, - set_field={1, 2, 3}, - ) - - model2 = StructuredModel( - tuple_field=("hello", 42), - list_field=["a", "b", "c"], - dict_field={"key1": "value1", "key2": 123}, - set_field={1, 2, 3}, - ) - - # Models with identical content should have same hash - assert hash(model1) == hash(model2) - - # Changing a tuple element should result in a different hash - model3 = StructuredModel( - tuple_field=("world", 42), # Different first element - list_field=["a", "b", "c"], - dict_field={"key1": "value1", "key2": 123}, - set_field={1, 2, 3}, - ) - assert hash(model1) != hash(model3) - - # Changing list order should result in a different hash - model4 = StructuredModel( - tuple_field=("hello", 42), - list_field=["a", "c", "b"], # Different order - dict_field={"key1": "value1", "key2": 123}, - set_field={1, 2, 3}, - ) - assert hash(model1) != hash(model4) - - # Adding a dict key should result in a different hash - model5 = StructuredModel( - tuple_field=("hello", 42), - list_field=["a", "b", "c"], - dict_field={"key1": "value1", "key2": 123, "key3": True}, # Additional key - set_field={1, 2, 3}, - ) - assert hash(model1) != hash(model5) - - # Changing set elements should result in a different hash - model6 = StructuredModel( - tuple_field=("hello", 42), - list_field=["a", "b", "c"], - dict_field={"key1": "value1", "key2": 123}, - set_field={1, 2, 4}, # 3 replaced with 4 - ) - assert hash(model1) != hash(model6) - - -def test_hash_with_nested_models() -> None: - """Test that models with nested model structures hash correctly.""" - - class Address(ModelBase): - street: str - city: str - postal_code: Optional[str] = None - - class Person(ModelBase): - name: str - address: Address - tags: List[str] - metadata: Dict[str, Any] - - # Create two models with identical nested structures - address1 = Address(street="123 Main St", city="Springfield") - person1 = Person( - name="Alice", - address=address1, - tags=["employee", "manager"], - metadata={"id": 123, "active": True}, - ) - - address2 = Address(street="123 Main St", city="Springfield") - person2 = Person( - name="Alice", - address=address2, - tags=["employee", "manager"], - metadata={"id": 123, "active": True}, - ) - - # Different address object but same content - assert address1 is not address2 - - # Models with identical content (including nested structures) should have same hash - assert hash(person1) == hash(person2) - - # Test a model with a different nested model - person3 = Person( - name="Alice", - address=Address(street="456 Elm St", city="Springfield"), # Different street - tags=["employee", "manager"], - metadata={"id": 123, "active": True}, - ) - - assert hash(person1) != hash(person3) - - # Test a model with nullable nested fields - address_with_postal = Address(street="123 Main St", city="Springfield", postal_code="12345") - person_with_postal = Person( - name="Alice", - address=address_with_postal, - tags=["employee", "manager"], - metadata={"id": 123, "active": True}, - ) - - assert hash(person1) != hash(person_with_postal) - - # Test with deeply nested structures - deeply_nested_person = Person( - name="Bob", - address=address1, - tags=["employee", "manager"], - metadata={ - "id": 456, - "active": True, - "history": { - "previous_roles": ["intern", "associate"], - "performance": {"2022": "Excellent", "2023": "Outstanding"}, - }, - }, - ) - - # Should be hashable despite complex nested structure - hash_value = hash(deeply_nested_person) - assert isinstance(hash_value, int) - - # Two identical deep structures should hash the same - deeply_nested_person2 = Person( - name="Bob", - address=address1, - tags=["employee", "manager"], - metadata={ - "id": 456, - "active": True, - "history": { - "previous_roles": ["intern", "associate"], - "performance": {"2022": "Excellent", "2023": "Outstanding"}, - }, - }, - ) - - assert hash(deeply_nested_person) == hash(deeply_nested_person2) diff --git a/tests/test_page_iterator.py b/tests/test_page_iterator.py deleted file mode 100644 index 0190f99bd..000000000 --- a/tests/test_page_iterator.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from typing import Any -from typing import AsyncIterator -from typing import Optional - -import pytest - -from foundry_sdk._core.page_iterator import AsyncPageIterator -from foundry_sdk._core.page_iterator import PageIterator - - -def create_page_func(total_items: int, default_page_size: int): - def page_function(page_size: Optional[int], next_page_token: Optional[str]): - page_size = page_size or default_page_size - next_page_token_int = int(next_page_token or "0") - - items = list(range(next_page_token_int, min(next_page_token_int + page_size, total_items))) - next_page_token = ( - str(next_page_token_int + page_size) - if next_page_token_int + page_size < total_items - else None - ) - - return (next_page_token, items) - - return page_function - - -def create_async_page_func(total_items: int, default_page_size: int): - async def page_function(page_size: Optional[int], next_page_token: Optional[str]): - page_size = page_size or default_page_size - next_page_token_int = int(next_page_token or "0") - - items = list(range(next_page_token_int, min(next_page_token_int + page_size, total_items))) - next_page_token = ( - str(next_page_token_int + page_size) - if next_page_token_int + page_size < total_items - else None - ) - - return (next_page_token, items) - - return page_function - - -def create_iterator(total_items: int, default_page_size: int): - return PageIterator[int](create_page_func(total_items, default_page_size)) - - -def create_async_iterator(total_items: int, default_page_size: int): - return AsyncPageIterator[int](create_async_page_func(total_items, default_page_size)) - - -def test_empty_iterator(): - iterator = create_iterator(0, 5) - assert iterator.data == [] - assert iterator.next_page_token is None - assert list(iterator) == [] - - -def test_iterator_with_one_item(): - iterator = create_iterator(1, 5) - assert iterator.data == [0] - assert iterator.next_page_token is None - assert list(iterator) == [] - - -def test_iterator_with_5_pages_of_5(): - iterator = create_iterator(25, 5) - - assert iterator.data == [0, 1, 2, 3, 4] - assert iterator.next_page_token == "5" - assert next(iterator) == [0, 1, 2, 3, 4] - assert iterator.next_page_token == "10" - - assert next(iterator) == [5, 6, 7, 8, 9] - assert iterator.next_page_token == "15" - - # Make sure it finishes the last 2 pages - assert len(list(iterator)) == 2 - - # And then confirm there is nothing left - with pytest.raises(StopIteration): - next(iterator) - - -async def alist(iterator: AsyncIterator[Any]) -> Any: - return [gen async for gen in iterator] - - -@pytest.mark.asyncio(scope="session") -async def test_empty_async_iterator(): - iterator = create_async_iterator(0, 5) - assert await iterator.get_data() == [] - assert await iterator.get_next_page_token() is None - assert await alist(iterator) == [[]] - - -@pytest.mark.asyncio(scope="session") -async def test_async_iterator_with_one_item(): - iterator = create_async_iterator(1, 5) - assert await iterator.get_data() == [0] - assert await iterator.get_next_page_token() is None - assert await alist(iterator) == [[0]] - - -@pytest.mark.asyncio(scope="session") -async def test_async_iterator_with_5_pages_of_5(): - iterator = create_async_iterator(25, 5) - - assert await iterator.get_data() == [0, 1, 2, 3, 4] - assert await iterator.get_next_page_token() == "5" - assert await iterator.__anext__() == [0, 1, 2, 3, 4] - assert await iterator.get_next_page_token() == "5" - - assert await iterator.__anext__() == [5, 6, 7, 8, 9] - assert await iterator.get_next_page_token() == "10" - - # Make sure it finishes the last 3 pages - assert len(await alist(iterator)) == 3 - - # And then confirm there is nothing left - with pytest.raises(StopAsyncIteration): - await iterator.__anext__() diff --git a/tests/test_performance.py b/tests/test_performance.py index ba0d8255d..57d5d31ac 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -1,3 +1,18 @@ +# Copyright 2024 Palantir Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + import timeit import pytest diff --git a/tests/test_resorce_import.py b/tests/test_resource_import.py similarity index 96% rename from tests/test_resorce_import.py rename to tests/test_resource_import.py index dc0159270..035dd2116 100644 --- a/tests/test_resorce_import.py +++ b/tests/test_resource_import.py @@ -1,3 +1,18 @@ +# Copyright 2024 Palantir Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + def test_datasets_v1_branch_import(): from foundry_sdk.v1.datasets.branch import BranchClient diff --git a/tests/test_resource_iterator.py b/tests/test_resource_iterator.py deleted file mode 100644 index 30fa17f6f..000000000 --- a/tests/test_resource_iterator.py +++ /dev/null @@ -1,215 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from typing import Optional - -import pytest - -from foundry_sdk._core.resource_iterator import AsyncResourceIterator -from foundry_sdk._core.resource_iterator import ResourceIterator - -from .test_page_iterator import alist -from .test_page_iterator import create_async_page_func -from .test_page_iterator import create_page_func - - -def create_iterator(total_items: int, default_page_size: int): - return ResourceIterator[int](create_page_func(total_items, default_page_size)) - - -def create_async_iterator(total_items: int, default_page_size: int): - return AsyncResourceIterator[int](create_async_page_func(total_items, default_page_size)) - - -def test_empty_iterator(): - iterator = create_iterator(0, 5) - assert iterator.data == [] - assert iterator.next_page_token is None - assert list(iterator) == [] - - -def test_iterator_with_one_item(): - iterator = create_iterator(1, 5) - assert iterator.data == [0] - assert iterator.next_page_token is None - assert list(iterator) == [0] - - -def test_iterator_with_5_pages_of_5(): - iterator = create_iterator(25, 5) - - # Check it can traverse from page to page correctly - # Page 1 - assert iterator.data == [0, 1, 2, 3, 4] - assert iterator.next_page_token == "5" - assert next(iterator) == 0 - assert next(iterator) == 1 - assert next(iterator) == 2 - assert next(iterator) == 3 - assert next(iterator) == 4 - assert iterator.data == [0, 1, 2, 3, 4] - - # Page 1 - assert next(iterator) == 5 - assert iterator.data == [5, 6, 7, 8, 9] - assert iterator.next_page_token == "10" - assert next(iterator) == 6 - assert next(iterator) == 7 - assert next(iterator) == 8 - assert next(iterator) == 9 - - # Make sure it finishes the last 3 pages - assert len(list(iterator)) == 15 - - # And then confirm there is nothing left - with pytest.raises(StopIteration): - next(iterator) - - -@pytest.mark.asyncio(scope="session") -async def test_empty_async_iterator(): - iterator = create_async_iterator(0, 5) - assert await iterator._page_iterator.get_data() == [] - assert await iterator._page_iterator.get_next_page_token() is None - assert await alist(iterator) == [] - - -@pytest.mark.asyncio(scope="session") -async def test_async_iterator_with_one_item(): - iterator = create_async_iterator(1, 5) - assert await iterator._page_iterator.get_data() == [0] - assert await iterator._page_iterator.get_next_page_token() is None - assert await alist(iterator) == [0] - - -@pytest.mark.asyncio(scope="session") -async def test_async_iterator_with_5_pages_of_5(): - iterator = create_async_iterator(25, 5) - - # Check it can traverse from page to page correctly - # Page 1 - assert await iterator.__anext__() == 0 - assert await iterator.__anext__() == 1 - assert await iterator.__anext__() == 2 - assert await iterator.__anext__() == 3 - assert await iterator.__anext__() == 4 - assert await iterator._page_iterator.get_data() == [0, 1, 2, 3, 4] - - # Page 1 - assert await iterator.__anext__() == 5 - assert await iterator.__anext__() == 6 - assert await iterator.__anext__() == 7 - assert await iterator.__anext__() == 8 - assert await iterator.__anext__() == 9 - assert await iterator._page_iterator.get_data() == [5, 6, 7, 8, 9] - - # Make sure it finishes the last 3 pages - assert len(await alist(iterator)) == 15 - - # And then confirm there is nothing left - with pytest.raises(StopAsyncIteration): - await iterator.__anext__() - - -def test_iterator_with_empty_page_in_middle(): - def page_func_with_empty_page(page_size: Optional[int], next_page_token: Optional[str]): - """Page function that returns: [0,1,2], [], [3,4,5], None""" - page_token = next_page_token or "page1" - - if page_token == "page1": - return ("page2", [0, 1, 2]) - elif page_token == "page2": - # Empty page but pagination continues - return ("page3", []) - elif page_token == "page3": - return (None, [3, 4, 5]) - else: - return (None, []) - - iterator = ResourceIterator[int](page_func_with_empty_page) - - # Should successfully iterate through all items, skipping the empty page - result = list(iterator) - assert result == [0, 1, 2, 3, 4, 5] - - -@pytest.mark.asyncio(scope="session") -async def test_async_iterator_with_empty_page_in_middle(): - async def async_page_func_with_empty_page( - page_size: Optional[int], next_page_token: Optional[str] - ): - """Async page function that returns: [0,1,2], [], [3,4,5], None""" - page_token = next_page_token or "page1" - - if page_token == "page1": - return ("page2", [0, 1, 2]) - elif page_token == "page2": - # Empty page but pagination continues - return ("page3", []) - elif page_token == "page3": - return (None, [3, 4, 5]) - else: - return (None, []) - - iterator = AsyncResourceIterator[int](async_page_func_with_empty_page) - - # Should successfully iterate through all items, skipping the empty page - result = await alist(iterator) - assert result == [0, 1, 2, 3, 4, 5] - - -def test_iterator_with_initial_page_token(): - def page_func(page_size: Optional[int], next_page_token: Optional[str]): - """Page function that returns different data based on page_token""" - page_token = next_page_token or "page1" - - if page_token == "page1": - return ("page2", [0, 1, 2]) - elif page_token == "page2": - return ("page3", [3, 4, 5]) - elif page_token == "page3": - return (None, [6, 7, 8]) - else: - return (None, []) - - # Start from page2 instead of page1 - iterator = ResourceIterator[int](page_func, page_token="page2") - - # Should only get items from page2 onwards: [3, 4, 5, 6, 7, 8] - result = list(iterator) - assert result == [3, 4, 5, 6, 7, 8] - - -@pytest.mark.asyncio(scope="session") -async def test_async_iterator_with_initial_page_token(): - async def async_page_func(page_size: Optional[int], next_page_token: Optional[str]): - """Async page function that returns different data based on page_token""" - page_token = next_page_token or "page1" - - if page_token == "page1": - return ("page2", [0, 1, 2]) - elif page_token == "page2": - return ("page3", [3, 4, 5]) - elif page_token == "page3": - return (None, [6, 7, 8]) - else: - return (None, []) - - # Start from page2 instead of page1 - iterator = AsyncResourceIterator[int](async_page_func, page_token="page2") - - # Should only get items from page2 onwards: [3, 4, 5, 6, 7, 8] - result = await alist(iterator) - assert result == [3, 4, 5, 6, 7, 8] diff --git a/tests/test_response_types.py b/tests/test_response_types.py deleted file mode 100644 index 632baee02..000000000 --- a/tests/test_response_types.py +++ /dev/null @@ -1,650 +0,0 @@ -# Copyright 2024 Palantir Technologies, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import decimal -import json -from datetime import date -from datetime import datetime -from typing import Any -from typing import Dict -from typing import Generic -from typing import List -from typing import Literal -from typing import Optional -from typing import TypeVar -from typing import Union - -import httpx -import pydantic -import pytest -from typing_extensions import Annotated - -from foundry_sdk._core import ApiResponse -from foundry_sdk._core import RequestInfo -from foundry_sdk._core.utils import RID -from foundry_sdk._core.utils import UUID -from foundry_sdk._core.utils import AwareDatetime -from foundry_sdk._core.utils import Long - - -# Simple BaseModel types for testing -class SimpleModel(pydantic.BaseModel): - value: str - - -class ModelWithAnnotation(pydantic.BaseModel): - value: Annotated[str, pydantic.Field(min_length=3)] - - -# Union type with discriminator -class RunningStatus(pydantic.BaseModel): - type: Literal["RUNNING"] = "RUNNING" - percent_complete: int - - -class FailedStatus(pydantic.BaseModel): - type: Literal["FAILED"] = "FAILED" - error_message: str - - -class CompletedStatus(pydantic.BaseModel): - type: Literal["COMPLETED"] = "COMPLETED" - result: str - - -# Define Status using Annotated for discriminator -Status = Annotated[ - Union[RunningStatus, FailedStatus, CompletedStatus], - pydantic.Field(discriminator="type"), -] - -# Optional Annotated Union -OptionalStatus = Optional[Status] - - -def create_response(response_type: Any, json_content: Any) -> ApiResponse: - """Helper function to create ApiResponse objects for testing.""" - return ApiResponse( - RequestInfo.with_defaults("GET", "/test/endpoint", response_type=response_type), - httpx.Response(200, content=json.dumps(json_content).encode()), - ) - - -def create_bytes_response(response_type: Any, content: bytes) -> ApiResponse: - """Helper function to create ApiResponse objects with raw bytes for testing.""" - return ApiResponse( - RequestInfo.with_defaults("GET", "/test/endpoint", response_type=response_type), - httpx.Response(200, content=content), - ) - - -# ----- Tests for Simple Types ----- - - -def test_decode_bytes(): - """Test decoding raw bytes.""" - response = create_bytes_response(bytes, b"raw bytes content") - result = response.decode() - assert result == b"raw bytes content" - assert isinstance(result, bytes) - - -def test_decode_optional_bytes_present(): - """Test decoding Optional[bytes] when content is present.""" - response = create_bytes_response(Optional[bytes], b"optional bytes content") - result = response.decode() - assert result == b"optional bytes content" - assert isinstance(result, bytes) - - -def test_decode_optional_bytes_empty(): - """Test decoding Optional[bytes] when content is empty.""" - response = create_bytes_response(Optional[bytes], b"") - result = response.decode() - assert result is None - - -def test_decode_none(): - response = create_response(None, "foo") - result = response.decode() - assert result is None - - -def test_decode_any_type(): - """Test decoding Any.""" - json_data = {"key": "value", "number": 42} - response = create_response(Any, json_data) - result = response.decode() - assert result == json_data - - json_data = {"key": "value", "number": 42} - response = create_response(Annotated[Any, pydantic.Field(description="test")], json_data) - result = response.decode() - assert result == json_data - - -def test_decode_string(): - """Test decoding a string.""" - json_data = "string value" - response = create_response(str, json_data) - result = response.decode() - assert result == json_data - assert isinstance(result, str) - - -def test_decode_integer(): - """Test decoding an integer.""" - json_data = 42 - response = create_response(int, json_data) - result = response.decode() - assert result == json_data - assert isinstance(result, int) - - -def test_decode_float(): - """Test decoding a float.""" - json_data = 42.5 - response = create_response(float, json_data) - result = response.decode() - assert result == json_data - assert isinstance(result, float) - - -def test_decode_boolean(): - """Test decoding a boolean.""" - json_data = True - response = create_response(bool, json_data) - result = response.decode() - assert result == json_data - assert isinstance(result, bool) - - -def test_decode_literal(): - """Test decoding a literal type.""" - json_data = "option1" - response = create_response(Literal["option1", "option2", "option3"], json_data) - result = response.decode() - assert result == json_data - assert result == "option1" - - -# ----- Tests for Collection Types ----- - - -def test_decode_list(): - """Test decoding a list of values.""" - json_data = ["item1", "item2", "item3"] - response = create_response(List[str], json_data) - result = response.decode() - assert result == json_data - assert isinstance(result, list) - assert all(isinstance(item, str) for item in result) - - -def test_decode_list_of_models(): - """Test decoding a list of models.""" - json_data = [{"value": "test1"}, {"value": "test2"}] - response = create_response(List[SimpleModel], json_data) - result = response.decode() - assert isinstance(result, list) - assert all(isinstance(item, SimpleModel) for item in result) - assert result[0].value == "test1" - assert result[1].value == "test2" - - -def test_decode_annotated_list(): - """Test decoding a list with length annotations.""" - json_data = ["item1", "item2", "item3"] - response = create_response( - Annotated[List[str], pydantic.Field(min_length=1, max_length=10)], - json_data, - ) - result = response.decode() - assert result == json_data - assert isinstance(result, list) - assert all(isinstance(item, str) for item in result) - - -def test_decode_dict(): - """Test decoding a dictionary.""" - json_data = {"key1": "value1", "key2": "value2"} - response = create_response(Dict[str, str], json_data) - result = response.decode() - assert result == json_data - assert isinstance(result, dict) - assert all(isinstance(key, str) and isinstance(value, str) for key, value in result.items()) - - -def test_decode_dict_with_model_values(): - """Test decoding a dictionary with model values.""" - json_data = {"key1": {"value": "test1"}, "key2": {"value": "test2"}} - response = create_response(Dict[str, SimpleModel], json_data) - result = response.decode() - assert isinstance(result, dict) - assert all( - isinstance(key, str) and isinstance(value, SimpleModel) for key, value in result.items() - ) - assert result["key1"].value == "test1" - assert result["key2"].value == "test2" - - -# ----- Tests for Special Types ----- - - -def test_decode_decimal(): - """Test decoding a decimal.Decimal.""" - json_data = "123.456" # Decimals are serialized as strings - response = create_response(decimal.Decimal, json_data) - result = response.decode() - assert isinstance(result, decimal.Decimal) - assert result == decimal.Decimal("123.456") - - -def test_decode_datetime(): - """Test decoding an AwareDatetime.""" - json_data = "2023-01-01T12:00:00Z" - response = create_response(AwareDatetime, json_data) - result = response.decode() - assert isinstance(result, datetime) - assert result.tzinfo is not None # Ensure it's timezone aware - assert result.year == 2023 - assert result.month == 1 - assert result.day == 1 - assert result.hour == 12 - - -def test_decode_date(): - """Test decoding a date.""" - json_data = "2023-01-01" - response = create_response(date, json_data) - result = response.decode() - assert isinstance(result, date) - assert result.year == 2023 - assert result.month == 1 - assert result.day == 1 - - -def test_decode_rid(): - """Test decoding a RID.""" - json_data = "ri.foundry.main.dataset.1234abcd" - response = create_response(RID, json_data) - result = response.decode() - assert isinstance(result, str) - assert result == json_data - - -def test_decode_uuid(): - """Test decoding a UUID.""" - json_data = "123e4567-e89b-12d3-a456-426614174000" - response = create_response(UUID, json_data) - result = response.decode() - assert isinstance(result, str) - assert result == json_data - - -def test_decode_long(): - """Test decoding a Long.""" - # Long values are typically integers that get serialized as strings in JSON - json_data = "9223372036854775807" # Max int64 value - response = create_response(Long, json_data) - result = response.decode() - assert isinstance(result, int) - assert result == 9223372036854775807 - - -# ----- Tests for Pydantic Models ----- - - -def test_decode_base_model(): - """Test decoding a simple BaseModel.""" - json_data = {"value": "test string"} - response = create_response(SimpleModel, json_data) - result = response.decode() - assert isinstance(result, SimpleModel) - assert result.value == "test string" - - -def test_decode_optional_base_model_present(): - """Test decoding Optional[BaseModel] when content is present.""" - json_data = {"value": "test string"} - response = create_response(Optional[SimpleModel], json_data) - result = response.decode() - assert isinstance(result, SimpleModel) - assert result.value == "test string" - - -def test_decode_optional_base_model_empty(): - """Test decoding Optional[BaseModel] when content is empty.""" - response = create_bytes_response(Optional[SimpleModel], b"") - result = response.decode() - assert result is None - - -def test_decode_annotated_base_model(): - """Test decoding an Annotated BaseModel.""" - json_data = {"value": "test string"} - response = create_response( - Annotated[SimpleModel, pydantic.Field(description="A test model")], - json_data, - ) - result = response.decode() - assert isinstance(result, SimpleModel) - assert result.value == "test string" - - -def test_decode_model_with_annotation(): - """Test decoding a model with annotated fields.""" - json_data = {"value": "test string"} - response = create_response(ModelWithAnnotation, json_data) - result = response.decode() - assert isinstance(result, ModelWithAnnotation) - assert result.value == "test string" - - -# ----- Tests for Union and Discriminated Types ----- - - -def test_decode_union_with_discriminator_running(): - """Test decoding a union type with discriminator (running status).""" - json_data = {"type": "RUNNING", "percent_complete": 75} - response = create_response(Status, json_data) - result = response.decode() - assert isinstance(result, RunningStatus) - assert result.type == "RUNNING" - assert result.percent_complete == 75 - - -def test_decode_union_with_discriminator_failed(): - """Test decoding a union type with discriminator (failed status).""" - json_data = {"type": "FAILED", "error_message": "Something went wrong"} - response = create_response(Status, json_data) - result = response.decode() - assert isinstance(result, FailedStatus) - assert result.type == "FAILED" - assert result.error_message == "Something went wrong" - - -def test_decode_union_with_discriminator_completed(): - """Test decoding a union type with discriminator (completed status).""" - json_data = {"type": "COMPLETED", "result": "Success!"} - response = create_response(Status, json_data) - result = response.decode() - assert isinstance(result, CompletedStatus) - assert result.type == "COMPLETED" - assert result.result == "Success!" - - -def test_decode_optional_union_with_discriminator_present(): - """Test decoding an Optional union type with discriminator when content is present.""" - json_data = {"type": "RUNNING", "percent_complete": 50} - response = create_response(OptionalStatus, json_data) - result = response.decode() - assert isinstance(result, RunningStatus) - assert result.type == "RUNNING" - assert result.percent_complete == 50 - - -def test_decode_optional_union_with_discriminator_empty(): - """Test decoding an Optional union type with discriminator when content is empty.""" - response = create_bytes_response(OptionalStatus, b"") - result = response.decode() - assert result is None - - -# ----- Tests for Nested and Complex Types ----- - - -def test_decode_annotated_optional_base_model(): - """Test decoding an Annotated Optional BaseModel.""" - json_data = {"value": "test string"} - response = create_response( - Annotated[Optional[SimpleModel], pydantic.Field(description="Optional model")], - json_data, - ) - result = response.decode() - assert isinstance(result, SimpleModel) - assert result.value == "test string" - - -def test_decode_optional_annotated_base_model(): - """Test decoding an Optional Annotated BaseModel.""" - json_data = {"value": "test string"} - response = create_response( - Optional[Annotated[SimpleModel, pydantic.Field(description="Annotated model")]], - json_data, - ) - result = response.decode() - assert isinstance(result, SimpleModel) - assert result.value == "test string" - - -def test_decode_optional_annotated_base_model_empty(): - """Test decoding an Optional Annotated BaseModel when content is empty.""" - response = create_bytes_response( - Optional[Annotated[SimpleModel, pydantic.Field(description="Annotated model")]], - b"", - ) - result = response.decode() - assert result is None - - -def test_decode_annotated_optional_annotated_base_model(): - """Test decoding an Annotated Optional Annotated BaseModel (deeply nested).""" - json_data = {"value": "test string"} - response = create_response( - Annotated[ - Optional[ - Annotated[ - SimpleModel, - pydantic.Field(description="Inner annotation"), - ] - ], - pydantic.Field(description="Outer annotation"), - ], - json_data, - ) - result = response.decode() - assert isinstance(result, SimpleModel) - assert result.value == "test string" - - -def test_decode_nested_unions(): - """Test decoding nested Union types.""" - json_data = {"type": "RUNNING", "percent_complete": 25} - nested_union = Union[Status, SimpleModel] - response = create_response(nested_union, json_data) - result = response.decode() - assert isinstance(result, RunningStatus) - assert result.type == "RUNNING" - assert result.percent_complete == 25 - - -def test_decode_list_of_optional_models(): - """Test decoding a list of optional models.""" - json_data = [{"value": "test1"}, None, {"value": "test3"}] - response = create_response(List[Optional[SimpleModel]], json_data) - result = response.decode() - assert isinstance(result, list) - assert isinstance(result[0], SimpleModel) - assert result[1] is None - assert isinstance(result[2], SimpleModel) - assert result[0].value == "test1" - assert result[2].value == "test3" - - -def test_decode_dict_with_complex_values(): - """Test decoding a dictionary with complex value types.""" - json_data = { - "model": {"value": "test"}, - "list": [1, 2, 3], - "nested": {"key": {"value": "nested value"}}, - } - response = create_response( - Dict[str, Union[SimpleModel, List[int], Dict[str, SimpleModel]]], - json_data, - ) - result = response.decode() - assert isinstance(result, dict) - assert isinstance(result["model"], SimpleModel) - assert isinstance(result["list"], list) - assert isinstance(result["nested"], dict) - assert isinstance(result["nested"]["key"], SimpleModel) - assert result["model"].value == "test" - assert result["list"] == [1, 2, 3] - assert result["nested"]["key"].value == "nested value" - - -# ----- Tests for Generic Types ----- - -T = TypeVar("T") - - -class GenericModel(pydantic.BaseModel, Generic[T]): - """A generic model for testing.""" - - value: T - - -def test_decode_generic_model(): - """Test decoding a generic model.""" - json_data = {"value": "test string"} - response = create_response(GenericModel[str], json_data) - result = response.decode() - assert isinstance(result, GenericModel) - assert result.value == "test string" - - json_data = {"value": 42} - response = create_response(GenericModel[int], json_data) - result = response.decode() - assert isinstance(result, GenericModel) - assert result.value == 42 - - -# ----- Tests for Type Mismatches ----- - - -def test_decode_string_when_int_expected(): - """Test decoding a string when an int was expected.""" - json_data = "not an integer" - response = create_response(int, json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_int_when_string_expected(): - """Test decoding an int when a string was expected.""" - json_data = 42 - response = create_response(str, json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_wrong_union_discriminator_value(): - """Test decoding a union with a discriminator value that doesn't match any option.""" - json_data = {"type": "UNKNOWN", "some_field": "value"} - response = create_response(Status, json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_missing_union_discriminator(): - """Test decoding a union with a missing discriminator field.""" - json_data = {"some_field": "value"} - response = create_response(Status, json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_list_with_wrong_element_type(): - """Test decoding a list where elements don't match expected type.""" - json_data = ["string", 123, True] # Mixed types - response = create_response(List[int], json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_model_missing_required_fields(): - """Test decoding a model with missing required fields.""" - json_data = {} # Missing required 'value' field - response = create_response(SimpleModel, json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_non_json_as_model(): - """Test decoding non-JSON data as a model.""" - response = create_bytes_response(SimpleModel, b"Not a JSON object") - with pytest.raises(json.JSONDecodeError): - response.decode() - - -def test_decode_wrong_datetime_format(): - """Test decoding a datetime with wrong format.""" - json_data = "01/01/2023" # Wrong format - response = create_response(AwareDatetime, json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_invalid_rid(): - """Test decoding an invalid RID.""" - json_data = "not-a-valid-rid" - response = create_response(RID, json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_dict_with_wrong_value_type(): - """Test decoding a dict with values of wrong type.""" - json_data = {"key1": 123, "key2": 456} # Numbers instead of strings - response = create_response(Dict[str, SimpleModel], json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_malformed_json_model(): - """Test decoding malformed JSON for a model.""" - json_data = {"value": {"nested": "object"}} # Value should be string not object - response = create_response(SimpleModel, json_data) - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_validation_error(): - """Test validation error when decoding a model with invalid data.""" - # This should fail validation because the value is too short (min_length=3) - json_data = {"value": "ab"} - response = create_response(ModelWithAnnotation, json_data) - - with pytest.raises(pydantic.ValidationError): - response.decode() - - -def test_decode_multiple_type_adapter_calls(): - """Test that the type adapter cache is working.""" - # Create multiple responses with the same type to test caching - json_data1 = {"type": "RUNNING", "percent_complete": 25} - json_data2 = {"type": "FAILED", "error_message": "Error message"} - - response1 = create_response(Status, json_data1) - result1 = response1.decode() - - response2 = create_response(Status, json_data2) - result2 = response2.decode() - - assert isinstance(result1, RunningStatus) - assert isinstance(result2, FailedStatus) - assert result1.type == "RUNNING" - assert result2.type == "FAILED" diff --git a/tests/test_utils.py b/tests/test_utils.py deleted file mode 100644 index 897ff663b..000000000 --- a/tests/test_utils.py +++ /dev/null @@ -1,185 +0,0 @@ -import typing -import warnings -from datetime import datetime -from datetime import timedelta -from datetime import timezone - -import pytest -import typing_extensions -from pydantic import BaseModel -from pydantic import ValidationError - -from foundry_sdk._core.utils import RID -from foundry_sdk._core.utils import UUID -from foundry_sdk._core.utils import AwareDatetime -from foundry_sdk._core.utils import Long -from foundry_sdk._core.utils import maybe_ignore_preview -from foundry_sdk._core.utils import remove_prefixes -from foundry_sdk._core.utils import resolve_forward_references - - -def test_remove_prefixes(): - assert remove_prefixes("http://example.com", ["https://", "http://"]) == "example.com" - assert remove_prefixes("https://example.com", ["https://", "http://"]) == "example.com" - assert remove_prefixes("example.com", ["https://", "http://"]) == "example.com" - - -def test_no_warning_when_preview_not_passed(): - @maybe_ignore_preview - def my_func_without_preview(preview: bool = False): - pass - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - my_func_without_preview() - assert len(w) == 0 # No warnings should be emitted - - -def test_no_warning_when_expected_preview(): - @maybe_ignore_preview - def my_func_without_preview(preview: bool = False): - pass - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - my_func_without_preview(preview=True) - assert len(w) == 0 # No warnings should be emitted - - -def test_warns_about_unexpected_preview(): - @maybe_ignore_preview - def my_func_without_preview(): - pass - - with pytest.warns( - UserWarning, - match=r'The "preview" argument is not required when calling my_func_without_preview\(\) since the endpoint is not in beta.', - ): - my_func_without_preview(preview=True) # type: ignore - - -def test_accepts_valid_rid(): - class WithRid(BaseModel): - rid: RID - - WithRid.model_validate({"rid": "ri.a.b.c.d"}) - WithRid.model_validate({"rid": "ri.foundry.main.dataset.b737e24d-6b19-43aa-93d5-da9fc4073f6"}) - - -def test_rejects_invalid_rid(): - class WithRid(BaseModel): - rid: RID - - with pytest.raises(ValidationError): - WithRid.model_validate({"rid": "ri.a.b.c"}) - - with pytest.raises(ValidationError): - WithRid.model_validate({"rid": "ri.foundry.main.0.b737e24d-6b19-43aa-93d5-da9fc4073f6"}) - - -def test_accepts_valid_uuid(): - class WithUuid(BaseModel): - uuid: UUID - - WithUuid.model_validate({"uuid": "b737e24d-6b19-43aa-93d5-da9fc4073f6e"}) - - -def test_rejects_invalid_uuid(): - class WithUuid(BaseModel): - uuid: UUID - - with pytest.raises(ValidationError): - WithUuid.model_validate({"uuid": "c"}) - - with pytest.raises(ValidationError): - WithUuid.model_validate({"uuid": "621f9a07-69e2-46c7-8015-c3bb8ee422e"}) - - -def test_accepts_valid_long(): - class WithLong(BaseModel): - long: Long - - WithLong.model_validate({"long": "1234"}) - WithLong.model_validate({"long": 1234}) - - -def test_rejects_invalid_long(): - class WithLong(BaseModel): - long: Long - - with pytest.raises(ValidationError): - WithLong.model_validate({"long": "a1234"}) - - -def test_long_serializes_to_string(): - class WithLong(BaseModel): - long: Long - - assert WithLong(long=123).model_dump_json() == '{"long":"123"}' - - -def test_accepts_valid_datetime(): - class WithDatetime(BaseModel): - datetime: AwareDatetime - - WithDatetime.model_validate({"datetime": datetime.now(timezone.utc)}) - - -def test_rejects_invalid_datetime(): - class WithDatetime(BaseModel): - datetime: AwareDatetime - - with pytest.raises(ValidationError): - WithDatetime.model_validate({"datetime": datetime.now()}) - - -def test_datetime_serializes_to_string(): - class WithDatetime(BaseModel): - datetime: AwareDatetime - - t = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone.utc) - assert WithDatetime(datetime=t).model_dump_json() == '{"datetime":"2023-10-01T12:00:00+00:00"}' - - -def test_non_utc_datetime_serializes_to_utc_string(): - class WithDatetime(BaseModel): - datetime: AwareDatetime - - t = datetime(2023, 10, 1, 12, 0, 0, tzinfo=timezone(timedelta(hours=2))) - assert WithDatetime(datetime=t).model_dump_json() == '{"datetime":"2023-10-01T10:00:00+00:00"}' - - -def test_resolve_dict_forward_references(): - A = typing.Dict[str, "B"] - B = str - - assert A == typing.Dict[str, "B"] - resolve_forward_references(A, globals(), locals()) - assert A == typing.Dict[str, str] - - -def test_resolve_annotated_union_forward_references(): - A = typing_extensions.Annotated[typing.Union["B", "C"], "Foo Bar"] - B = str - C = int - - resolve_forward_references(A, globals(), locals()) - assert A == typing_extensions.Annotated[typing.Union[str, int], "Foo Bar"] - - -def test_resolve_duplicate_forward_references(): - A = typing.List["C"] - B = typing.List["C"] - C = typing.List[float] - - resolve_forward_references(B, globals(), locals()) - resolve_forward_references(A, globals(), locals()) - assert A == typing.List[typing.List[float]] - - -def test_resolve_double_forward_reference(): - A = typing.List[typing.List["B"]] - B = float - - resolve_forward_references(A, globals(), locals()) - assert A == typing.List[typing.List[float]]